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
- A text editor or IDE
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 comparable to other systems programming languages
- 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
Security Best Practices
When deploying your API, consider implementing:
- Rate limiting to prevent abuse
- Input validation for all endpoints
- HTTPS encryption
- Cross-Origin Resource Sharing (CORS) configuration
- Security headers
- Request sanitization
Scaling Considerations
As your API grows, consider these scaling strategies:
- Implement caching for frequently accessed data
- Use load balancers for horizontal scaling
- Set up monitoring and alerting
- Use CDN for static assets
Monitoring and Observability
For production deployments, implement:
- Structured logging with different severity levels
- Metrics collection for performance monitoring
- Distributed tracing for request flows
- Error tracking and alerting
- Health check endpoints
- Resource usage monitoring
Deployment Best Practices
Consider these deployment strategies:
- Use container orchestration (Kubernetes)
- Implement blue-green deployments
- Set up automated rollbacks
- Use infrastructure as code
- Implement proper secret management
- Set up backup and recovery procedures
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.