· 14 min read

What’s new in Hummingbird 2?

Hummingbird is a lightweight, flexible HTTP server framework written in Swift. The work on the second major version started halfway through 2023 and the first alpha version was tagged on the 22th of January, 2024. There are quite a lot of significant changes and under the hood improvements. With 2.0 being released, this is a great opportunity to read up on it!

Swift concurrency

Hummingbird 2 was built using the modern Swift concurrency APIs. Most of the NIO event loop references are replaced with async / await functions and calls. Structured concurrency is present all around the codebase and the project components, such as Request, are thread safe thanks to the Sendable conformance.

Before the async / await feature adoption, some components had a HBAsync prefix. Those are now removed from the v2 library. For example HBAsyncMiddleware is now MiddlewareProtocol or HBAsyncResponder is simply called HTTPResponder.

It is worth to mention that HB2 is prepared for Swift 6, the project also compiles against the experimental StrictConcurrency=complete feature flag.

Swift service lifecycle v2

The Swift service lifecycle library provides a clean startup and shutdown mechanism for server applications. Hummingbird 2 uses the latest version of the library including support for graceful shutdown even for custom application services. When Hummingbird is signalled by swift-service-lifecycle to gracefully shut down, any currently running requests continue being handled. New connections and requests will not be accepted, and idle connections are shut down. Once everything’s ready, Hummingbird will shut down completely.

Hummingbird core and foundation

The HummingbirdCore repository is merged into main repository. The HummingbirdFoundation target was also removed and now all the Foundation extensions are part of the main Hummingbird Swift package target. This makes Hummingbird ergonomically closer to Vapor, allowing users to get started more quickly. This decision is backed by the upcoming move to the new swift-foundation library.

Jobs framework updates

The HummingbirdJobs framework can be used to push work onto a queue, so that is processed outside of a request. Job handlers were restructured to use TaskGroup and conform to the Service protocol from the Swift service lifecycle framework. A JobQueue can also define it’s own JobID type, which helps when integrating with various database/driver implementations.

Connection pools

The custom connection pool implementation was removed from the framework. Previously, this component offered connection pooling for PostgreSQL. Since PostgresNIO has built-in support, there’s no need for it anymore inside the HB framework.

HTTP improvements

Hummingbird 2 takes advantage of the brand new Swift HTTP Types library. The overall support for HTTP2 and TLS is also improved a lot in the second major version.

Router library

Hummingbird 2 features a brand new routing library, based on Swift result builders. This is a standalone project, the old route building mechanism still works, but if you prefer result builders you can try the new method by importing this lib.

Here’s a little peak into the usage of the new RouterBuilder object:

import Hummingbird
import HummingbirdRouter

let router = RouterBuilder(context: BasicRouterRequestContext.self) {
    TracingMiddleware()
    Get("test") { _, context in
        return context.endpointPath
    }
    Get { _, context in
        return context.endpointPath
    }
    Post("/test2") { _, context in
        return context.endpointPath
    }
}
let app = Application(responder: router)
try await app.runService()
hummingbird-2-routerbuilder.swift

Generic request context

The biggest change to the framework is definitely the introduction of the generic request context. Hummingbird 2.0 separates contextual objects from the Request type and users can define custom properties as custom RequestContext protocol implementations.

The request context is associated with the reworked Router, which a generic class, featuring a Context type. The BasicRequestContext type is the default Context implementation for the Router. The request decoder and encoder defaults to a JSON-based solution when using the base context. You can provide a custom decoder through a custom router context.

Let me show you how this new contextual router system works in practice.

HB2 example project

This article contains a sample project, which you can download from the following link.

You can integrate Hummingbird 2 by adding it as a dependency to your project, using Swift Package Manager.

// swift-tools-version:5.9
import PackageDescription

let package = Package(
    name: "whats-new-in-hummingbird-2-sample",
    platforms: [
        .macOS(.v14),
    ],
    dependencies: [
        .package(url: "https://github.com/hummingbird-project/hummingbird.git", from: "2.0.0"),
        .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.4.0"),
    ],
    targets: [
        .executableTarget(
            name: "App",
            dependencies: [
                .product(name: "ArgumentParser", package: "swift-argument-parser"),
                .product(name: "Hummingbird", package: "hummingbird"),
                // .product(name: "HummingbirdRouter", package: "hummingbird"),
            ]
        ),
        .testTarget(
            name: "AppTests",
            dependencies: [
                .target(name: "App"),
                .product(name: "HummingbirdXCT", package: "hummingbird"),
            ]
        ),
    ]
)

Here’s how to build a custom decoder to handle different media types on your backend server:

// 1.
struct MyRequestDecoder: RequestDecoder {
    func decode<T>(
        _ type: T.Type,
        from request: Request,
        context: some RequestContext
    ) async throws -> T where T: Decodable {
        // 2.
        guard let header = request.headers[.contentType] else {
            throw HTTPError(.badRequest)
        }
        // 3.
        guard let mediaType = MediaType(from: header) else {
            throw HTTPError(.badRequest)
        }
        // 4.
        let decoder: RequestDecoder
        switch mediaType {
        case .applicationJson:
            decoder = JSONDecoder()
        case .applicationUrlEncoded:
            decoder = URLEncodedFormDecoder()
        default:
            throw HTTPError(.badRequest)
        }
        // 5
        return try await decoder.decode(
            type,
            from: request,
            context: context
        )
    }
}
hummingbird-2.swift:9
  1. Define the custom decoder by implementing the RequestDecoder protocol.

  2. Make sure that the incoming request has a Content-Type HTTP header field.

  3. Construct a valid MediaType object from the header field.

  4. Setup a custom decoder based on the media type.

  5. Return the decoded object using the decoder, with the request and the context.

To use the custom decoder, let’s define a custom request context. A request context is a container for the Hummingbird framework to store information needed by the framework. The following snippet demonstrates how to build one using the RequestContext protocol:

// 1.
protocol MyRequestContext: RequestContext {
    var myValue: String? { get set }
}

// 2.
struct MyBaseRequestContext: MyRequestContext {
    var coreContext: CoreRequestContextStorage

    // 3.
    var myValue: String?

    init(source: Source) {
        self.coreContext = .init(source: source)
    }

    // 4.
    var requestDecoder: RequestDecoder {
        MyRequestDecoder()
    }
}
hummingbird-2.swift:44
  1. Define a custom MyRequestContext protocol using the RequestContext protocol.

  2. Implement the MyRequestContext protocol using a MyBaseRequestContext struct.

  3. Implement custom properties, configure them using the init method, if needed.

  4. Return the custom MyRequestDecoder as a default request decoder implementation.

The HummingbirdAuth library also defines a custom context (AuthRequestContext) in a similar way to store user auth information.

It is possible to compose multiple protocols such as AuthRequestContext by conforming to all of them. This makes it easy to integrate the context with various libraries. This also allows libraries to provide middleware that accept a custom context as input, or that modify a custom context, to enrich requests. For example, enriching a request by adding the authenticated user.

Create the application instance using the buildApplication function.

func buildApplication() async throws -> some ApplicationProtocol {
    // 1.
    let router = Router(context: MyBaseRequestContext.self)

    // 2
    router.middlewares.add(LogRequestsMiddleware(.info))
    router.middlewares.add(FileMiddleware())
    router.middlewares.add(
        CORSMiddleware(
            allowOrigin: .originBased,
            allowHeaders: [.contentType],
            allowMethods: [.get, .post, .delete, .patch]
        )
    )

    // 3
    router.get("/health") { _, _ -> HTTPResponse.Status in
        .ok
    }

    // 4.
    MyController().addRoutes(to: router.group("api"))

    // 5.
    return Application(
        router: router,
        configuration: .init(
            address: .hostname("localhost", port: 8080)
        )
    )
}

@main
struct HummingbirdArguments: AsyncParsableCommand {
    func run() async throws {
        let app = try await buildApplication()
        try await app.runService()
    }
}
hummingbird-2.swift:109
  1. Setup the router using the MyBaseRequestContext type as a custom context.

  2. Add middlewares to the router, HB2 has middlewares on the router instead of the app

  3. Setup a basic health route on the router, simply return with a HTTP status code

  4. Add routes using the custom controller to the api route group

  5. Build the Application instance using the router and the configuration

Inside the main entrypoint you can start the server by calling the Application.runService() method:

import ArgumentParser
import Foundation
import Hummingbird
import Logging
import NIOCore
hummingbird-2.swift:2

The route handlers in the MyController struct can access of the custom context type.

struct MyController<Context: MyRequestContext> {
    // 1.
    func addRoutes(
        to group: RouterGroup<Context>
    ) {
        group
            .get(use: list)
            .post(use: create)
    }

    // 2.
    @Sendable
    func list(
        _ request: Request,
        context: Context
    ) async throws -> [MyModel] {
        [
            .init(title: "foo"),
            .init(title: "bar"),
            .init(title: "baz"),
        ]
    }

    @Sendable
    func create(
        _ request: Request,
        context: Context
    ) async throws -> EditedResponse<MyModel> {
        // 3.
        // context.myValue
        let input = try await request.decode(
            as: MyModel.self,
            context: context
        )
        return .init(status: .created, response: input)
    }
}
hummingbird-2.swift:70
  1. Register route handlers using the router group

  2. Hummingbird is thread-safe, so every route handler should be marked with @Sendable to propagate these thread-safety checks.

  3. It is possible to access both the request and the context in each route handler.

Beyond these changes, there are many more in the new Hummingbird 2 release. We’re excited for you to try it out!

If have questions about Hummingbird, feel free to join the following Discord server. You can also get some inspiration from the official Hummingbird examples.

Related posts

Using Hummingbird's Request Contexts

Learn about request contexts in Hummingbird and how to use them.

How to Build a Proxy Server with Hummingbird

Learn how to leverage the flexibility and performance of Hummingbird to build a proxy server.

Getting Started with Hummingbird

Learn how to get started with Hummingbird 2, the modern Swift Web Framework.

Working with UDP in SwiftNIO

Create UDP servers and clients using SwiftNIO and structured concurrency

Using WebSockets in Hummingbird

In this article, you will learn about WebSockets and how to use them with the Hummingbird framework in a straightforward, easy-to-follow manner.

Using OpenAPI Generator with Hummingbird

Learn how to use OpenAPI Generator to create Swift APIs with Hummingbird.

Server-Side Swift Conference logo

ServerSide.swift

The talk recordings are live!
See you next year!

Watch the Videos