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.swiftGeneric 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:9Define the custom decoder by implementing the
RequestDecoder
protocol.Make sure that the incoming request has a
Content-Type
HTTP header field.Construct a valid
MediaType
object from the header field.Setup a custom decoder based on the media type.
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:44Define a custom
MyRequestContext
protocol using the RequestContext protocol.Implement the
MyRequestContext
protocol using aMyBaseRequestContext
struct.Implement custom properties, configure them using the init method, if needed.
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:109Setup the router using the
MyBaseRequestContext
type as a custom context.Add middlewares to the router, HB2 has middlewares on the router instead of the app
Setup a basic health route on the router, simply return with a HTTP status code
Add routes using the custom controller to the
api
route groupBuild 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:2The 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:70Register route handlers using the router group
Hummingbird is thread-safe, so every route handler should be marked with
@Sendable
to propagate these thread-safety checks.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
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.
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.