Building a Simple REST API with Swift and Hummingbird 2
In this tutorial, we'll explore how to build a REST API using Swift and Hummingbird 2. While Swift is primarily known for iOS development, it's becoming an increasingly popular choice for server-side applications thanks to its performance, safety features, and modern concurrency model.
The complete source code for this tutorial is available on GitHub.
Prerequisites
- Swift 6 or later installed on your machine
- Basic knowledge of Swift programming
- Familiarity with REST API concepts
Why Choose Swift for Backend Development?
Swift offers several compelling advantages for backend development:
- Strong type safety that catches errors at compile time
- Modern async/await concurrency model
- High performance and low memory footprint
- Familiar syntax for iOS developers enabling full-stack development
- Growing ecosystem of server-side frameworks and tools
Understanding Hummingbird
Hummingbird is a modern Swift web framework designed with performance and developer experience in mind. Unlike some other Swift web frameworks, Hummingbird 2 is built from the ground up to leverage Swift's latest features, particularly the async/await concurrency model.
Key benefits include:
- Native support for Swift concurrency
- Minimal overhead and high performance
- Intuitive routing system
- Strong type safety throughout the stack
- Excellent documentation and growing community
Project Architecture
Our API follows clean architecture principles with clear separation of concerns:
- Models: Define the data structures
- Repository: Handles data persistence
- Controllers: Manage HTTP requests and business logic
- Router: Defines API endpoints
This architecture makes the code maintainable, testable, and scalable.
Setting Up the Project
First, create a new Swift project:
mkdir swift-hummingbird-api
cd swift-hummingbird-api
swift package init --type executable
Update your Package.swift
:
// swift-tools-version: 6.0
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "swift-hummingbird-api",
platforms: [
.macOS(.v14),
],
products: [
.executable(name: "App", targets: ["App"]),
],
dependencies: [
.package(url: "https://github.com/hummingbird-project/hummingbird.git", from: "2.0.0"),
],
targets: [
.executableTarget(
name: "App",
dependencies: [.product(name: "Hummingbird", package: "hummingbird")]),
]
)
Basic API Structure
Let's define our model first in Sources/Models/Item.swift
:
import Foundation
import Hummingbird
struct Item: ResponseCodable {
let id: UUID
var name: String
var description: String
}
Implementing the repository
Let's create a simple in-memory repository in Sources/Repositories/ItemRepository.swift
:
import Foundation
protocol ItemRepository: Sendable {
func list() async -> [Item]
func create(name: String, description: String) async throws -> Item
func get(id: UUID) async -> Item?
func update(id: UUID, name: String?, description: String?) async throws -> Item?
func delete(id: UUID) async -> Bool
}
actor ItemMemoryRepository: ItemRepository {
private var items: [UUID: Item]
init() {
self.items = [:]
}
func list() async -> [Item] {
return Array(items.values)
}
func create(name: String, description: String) async throws -> Item {
let item = Item(id: UUID(), name: name, description: description)
self.items[item.id] = item
return item
}
func get(id: UUID) async -> Item? {
return self.items[id]
}
func update(id: UUID, name: String?, description: String?) async throws -> Item? {
if var item = self.items[id] {
if let name {
item.name = name
}
if let description {
item.description = description
}
self.items[id] = item
return item
}
return nil
}
func delete(id: UUID) async -> Bool {
guard let _ = self.items[id] else {
print("not found")
return false
}
print("Found")
self.items[id] = nil
return true
}
}
Implementing the Controller
Create your controller in Sources/Controllers/ItemController.swift
:
import Foundation
import Hummingbird
struct ItemController<Context: RequestContext> {
let repository: ItemRepository
func addRoutes(to group: RouterGroup<Context>) {
group
.get(use:self.list)
.get(":id", use: self.get)
.post(use: self.create)
.patch(":id", use: self.update)
.delete(":id", use: self.delete)
}
@Sendable
func list(_ request: Request, context: Context) async throws -> [Item] {
return await self.repository.list()
}
@Sendable
func get(_ request: Request, context: Context) async throws -> Item? {
let id = try context.parameters.require("id", as: UUID.self)
return await self.repository.get(id: id)
}
struct CreateItemRequest: Decodable {
let name: String
let description: String
}
@Sendable
func create(_ request: Request, context: Context) async throws -> EditedResponse<Item> {
let itemRequest = try await request.decode(as: CreateItemRequest.self, context: context)
let item = try await self.repository.create(name: itemRequest.name, description: itemRequest.description)
return .init(status: .created, response: item)
}
struct UpdateItemRequest: Decodable {
let name: String?
let description: String?
}
@Sendable
func update(_ request: Request, context: Context) async throws -> Item {
let id = try context.parameters.require("id", as: UUID.self)
let itemRequest = try await request.decode(as: UpdateItemRequest.self, context: context)
let updatedItem = try await self.repository.update(id: id, name: itemRequest.name, description: itemRequest.description)
guard let item = updatedItem else {
throw HTTPError(.notFound)
}
return item
}
@Sendable
func delete(_ request: Request, context: Context) async throws -> HTTPResponse.Status {
let id = try context.parameters.require("id", as: UUID.self)
let deleted = await self.repository.delete(id: id)
guard deleted else {
throw HTTPError(.notFound)
}
return .ok
}
}
Implementing the API
Create your main application file Sources/main.swift
:
import Hummingbird
import Foundation
let itemRepository: ItemRepository = ItemMemoryRepository()
let router = Router()
ItemController<BasicRequestContext>(repository: itemRepository).addRoutes(to: router.group("api/items"))
let app = Application(router: router)
try await app.run()
Testing Strategy
The included tests demonstrate basic API functionality, but a production application should include:
- Unit tests for business logic
- Integration tests for API endpoints
- Performance tests for critical paths
- Load tests for concurrent operations
Edit Package.swift
and add the test target Your Package.swift should look like this:
// swift-tools-version: 6.0
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "swift-hummingbird-api",
platforms: [
.macOS(.v14),
],
products: [
.executable(name: "App", targets: ["App"]),
],
dependencies: [
.package(url: "https://github.com/hummingbird-project/hummingbird.git", from: "2.0.0"),
],
targets: [
.executableTarget(
name: "App",
dependencies: [.product(name: "Hummingbird", package: "hummingbird")]),
.testTarget(
name: "AppTests",
dependencies: [
.byName(name: "App"),
.product(name: "HummingbirdTesting", package: "hummingbird"),
]
),
]
)
Create tests in Tests/AppTest.swift
:
import Foundation
import Hummingbird
import HummingbirdTesting
import Testing
@testable import App
struct AppTests {
@Test
func testCreate() async throws {
let itemRepository: ItemRepository = ItemMemoryRepository()
let router = Router()
ItemController<BasicRequestContext>(repository: itemRepository)
.addRoutes(to: router.group("api/items"))
let app = Application(router: router)
try await app.test(.router) { client in
try await client.execute(
uri: "/api/items", method: .post,
body: ByteBuffer(
string: #"{"name":"book", "description":"a nice book"}"#)
) { response in
#expect(response.status == .created)
}
}
}
}
Making Requests
Test your API using curl:
# Create new item
curl -i -X POST http://localhost:8080/api/items \
-H "Content-Type: application/json" \
-d '{"name":"New Item","description":"New Description"}'
# Get all items
curl -i http://localhost:8080/api/items
# Get specific item
curl -i http://localhost:8080/api/items/47050FDA-214F-4950-AE36-5780F61B2BC1
# Update an item
curl -i -X PATCH http://localhost:8080/api/items/DF014C66-C5C6-41DC-BACB-D1F47DD91332 \
-H "Content-Type: application/json" \
-d '{"name":"Updated Item","description":"Updated Description"}'
# Delete an item
curl -i -X DELETE http://localhost:8080/api/items/689B4B05-05A7-41FA-B53F-0885D74BC6D3
Conclusion
Swift and Hummingbird 2 provide a robust foundation for building modern web APIs. The combination of Swift's safety features and Hummingbird's performance makes it an excellent choice for building scalable backend services. While this tutorial covers the basics, the architecture can be extended to support more complex requirements in production environments.
Resources for Further Learning
- Official Hummingbird Documentation
- Swift Server Working Group
- Swift Package Manager Guide
- Swift Concurrency Documentation
Remember to always follow Swift best practices and keep your dependencies up to date for security and performance improvements.