Posted on :: Tags: , , ,

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

Remember to always follow Swift best practices and keep your dependencies up to date for security and performance improvements.