· 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(gracefulShutdownSignals:) 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

· 3 min read

Hummingbird and CORS

Learn how to use Hummingbird and CORS in Swift

· 8 min read

Realtime MongoDB Updates with ChangeStreams and WebSockets

Learn how to implement real-time updates over WebSockets using ChangeStreams and MongoKitten

· 10 min read

Beginner's Guide to Protocol Buffers and gRPC with Swift

Learn Protocol Buffers and gRPC with Swift in this easy, step-by-step beginner's guide.

· 6 min read

Using Hummingbird's Request Contexts

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

· 9 min read

How to Build a Proxy Server with Hummingbird

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

SwiftCraft logo

SwiftCraft

SwiftCraft is a UK based Swift Conference, 19th-21st May 2025!
There'll be talks, workshops, and a lot of Swift (Server) fun!

Get Tickets
struct Request

Holds all the values required to process a request

protocol Sendable
protocol MiddlewareProtocol<Input, Output, Context> : Sendable

Middleware protocol with generic input, context and output types

protocol HTTPResponder<Context> : Sendable

Protocol for object that produces a response given a request

@frozen struct TaskGroup<ChildTaskResult> where ChildTaskResult : Sendable

A group that contains dynamically created child tasks.

protocol Service : Sendable

This is the basic protocol that a service has to implement.

struct RouterBuilder<Context, Handler> where Context : RouterRequestContext, Context == Handler.Context, Handler : MiddlewareProtocol, Handler.Input == Request, Handler.Output == Response

Router built using a result builder

struct BasicRouterRequestContext

Basic implementation of a context that can be used with `RouterBuilder``

struct TracingMiddleware<Context> where Context : RequestContext

Middleware creating Distributed Tracing spans for each request.

func Get<RouteOutput, Context>(_ routerPath: RouterPath = "", handler: @escaping (Request, Context) async throws -> RouteOutput) -> Route<_RouteHandlerClosure<RouteOutput, Context>, Context> where RouteOutput : ResponseGenerator, Context : RouterRequestContext

Create a GET Route with a closure

init(stringLiteral value: String)

Initialize RouterPath from String literal

var endpointPath: String? { get }

Endpoint path

func Post<RouteOutput, Context>(_ routerPath: RouterPath = "", handler: @escaping (Request, Context) async throws -> RouteOutput) -> Route<_RouteHandlerClosure<RouteOutput, Context>, Context> where RouteOutput : ResponseGenerator, Context : RouterRequestContext

Create a POST Route with a closure

struct Application<Responder> where Responder : HTTPResponder, Responder.Context : InitializableFromSource, Responder.Context.Source == ApplicationRequestContextSource

Application class. Brings together all the components of Hummingbird together

func runService(gracefulShutdownSignals: [UnixSignal] = [.sigterm, .sigint]) async throws

Helper function that runs application inside a ServiceGroup which will gracefully shutdown on signals SIGINT, SIGTERM

final class Router<Context> where Context : RequestContext

Create rules for routing requests and then create Responder that will follow these rules.

protocol RequestDecoder

protocol for decoder deserializing from a Request body

protocol RequestContext : InitializableFromSource, RequestContextSource where Self.Source : RequestContextSource

Protocol that all request contexts should conform to. A RequestContext is a statically typed metadata container for information that is associated with a Request, and is therefore instantiated alongside the request.

protocol Decodable

A type that can decode itself from an external representation.

var headers: HTTPFields { get }

Request HTTP headers

subscript(name: HTTPField.Name) -> String? { get set }

Access the field value string by name.

static var contentType: `Self` { get }

Content-Type

struct HTTPError

Default HTTP error. Provides an HTTP status and a message

static var badRequest: `Self` { get }

400 Bad Request

struct MediaType

Define media type of file

static var applicationJson: Self { get }

JSON format

class JSONDecoder

JSONDecoder facilitates the decoding of JSON into semantic Decodable types.

static var applicationUrlEncoded: Self { get }

URL encoded form data

struct URLEncodedFormDecoder

The wrapper struct for decoding URL encoded form data to Codable classes

func decode<T>(_ type: T.Type, from request: Request, context: some RequestContext) async throws -> T where T : Decodable

Decode type from request

@frozen struct String

A Unicode string value that is a collection of characters.

struct CoreRequestContextStorage

Request context values required by Hummingbird itself.

associatedtype Source = ApplicationRequestContextSource
init(source: some RequestContextSource)
protocol AuthRequestContext<Identity> : RequestContext

Protocol that all request contexts should conform to if they want to support authentication middleware

protocol ApplicationProtocol : Service

Protocol for an Application. Brings together all the components of Hummingbird together

let middlewares: MiddlewareGroup<Context>
@discardableResult func add(_ middleware: any MiddlewareProtocol<Request, Response, Context>) -> Self

Add middleware to group

struct LogRequestsMiddleware<Context> where Context : RequestContext

Middleware outputting to log for every call to server.

case info

Appropriate for informational messages.

struct FileMiddleware<Context, Provider> where Context : RequestContext, Provider : FileProvider, Provider.FileAttributes : FileMiddlewareFileAttributes

Middleware for serving static files.

struct CORSMiddleware<Context> where Context : RequestContext

Middleware implementing Cross-Origin Resource Sharing (CORS) headers.

case originBased
static var get: `Self` { get }

GET

static var post: `Self` { get }

POST

static var delete: `Self` { get }

DELETE

static var patch: `Self` { get }

PATCH

@discardableResult func get(_ path: RouterPath = "", use handler: @escaping (Request, Context) async throws -> some ResponseGenerator) -> Self

GET path for async closure returning type conforming to ResponseGenerator

struct HTTPResponse

An HTTP response message consisting of the “:status” pseudo header field and header fields.

struct Status

The response status consisting of a 3-digit status code and a reason phrase. The reason phrase is ignored by modern HTTP versions.

static var ok: `Self` { get }

200 OK

func group(_ path: RouterPath = "") -> RouterGroup<Context>

Return a group inside the current group

init(address: BindAddress = .hostname(), serverName: String? = nil, backlog: Int = 256, reuseAddress: Bool = true, availableConnectionsDelegate: AvailableConnectionsDelegate? = nil)

Initialize Application configuration

static func hostname(_ host: String = "127.0.0.1", port: Int = 8080) -> BindAddress
protocol AsyncParsableCommand : ParsableCommand

A type that can be executed asynchronously, as part of a nested tree of commands.

struct RouterGroup<Context> where Context : RequestContext

Used to group together routes under a single path. Additional middleware can be added to the endpoint and each route can add a suffix to the endpoint path

@discardableResult func post(_ path: RouterPath = "", use handler: @escaping (Request, Context) async throws -> some ResponseGenerator) -> Self

POST path for async closure returning type conforming to ResponseGenerator

struct EditedResponse<Generator> where Generator : ResponseGenerator
func decode<Type>(as type: Type.Type, context: some RequestContext) async throws -> Type where Type : Decodable

Decode request using decoder stored at Application.decoder.

init(status: HTTPResponse.Status? = nil, headers: HTTPFields = .init(), response: Generator)
static var created: `Self` { get }

201 Created