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

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

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