| name | hummingbird |
| description | Expert guidance on Hummingbird 2 web framework. Use when developers mention: (1) Hummingbird, HB, or Hummingbird 2, (2) Swift web server or HTTP server, (3) server-side Swift routing or middleware, (4) building REST APIs in Swift, (5) RequestContext or ChildRequestContext, (6) HummingbirdAuth or authentication middleware, (7) HummingbirdWebSocket, (8) HummingbirdFluent or database integration, (9) ResponseGenerator or EditedResponse. |
Hummingbird 2
Hummingbird is a lightweight, flexible HTTP server framework for Swift, built on SwiftNIO with full Swift Concurrency support. It's an SSWG (Swift Server Work Group) incubated project.
Quick Start
Installation
Add to Package.swift:
dependencies: [
.package(url: "https://github.com/hummingbird-project/hummingbird.git", from: "2.0.0")
]
Add to your target:
.target(
name: "App",
dependencies: [
.product(name: "Hummingbird", package: "hummingbird")
]
)
Minimal Application
import Hummingbird
@main
struct App {
static func main() async throws {
let router = Router()
router.get("/") { _, _ in
"Hello, World!"
}
router.get("/health") { _, _ -> HTTPResponse.Status in
.ok
}
let app = Application(
router: router,
configuration: .init(address: .hostname("0.0.0.0", port: 8080))
)
try await app.runService()
}
}
Core Concepts
Router
The router directs requests to handlers based on path and HTTP method:
let router = Router()
router.get("/users") { request, context in
}
router.post("/users") { request, context in
}
router.get("/users/{id}") { request, context in
let id = context.parameters.get("id")!
}
router.put("/users/{id}") { request, context in
}
router.delete("/users/{id}") { request, context in
}
Route Groups
Organize routes with common prefixes:
let router = Router()
router.group("api/v1") { api in
api.group("users") { users in
users.get { _, _ in }
users.post { _, _ in }
users.get("{id}") { _, context in }
}
api.group("posts") { posts in
posts.get { _, _ in }
}
}
Request Context
Each request gets a context instance. Use BasicRequestContext or create custom contexts:
let router = Router(context: BasicRequestContext.self)
struct AppRequestContext: RequestContext {
var coreContext: CoreRequestContextStorage
var requestId: String?
var authenticatedUser: User?
init(source: Source) {
self.coreContext = .init(source: source)
}
}
let router = Router(context: AppRequestContext.self)
Child Request Context
Use ChildRequestContext to transform contexts and guarantee properties exist:
import HummingbirdAuth
struct AppRequestContext: AuthRequestContext {
var coreContext: CoreRequestContextStorage
var auth: LoginCache
init(source: Source) {
self.coreContext = .init(source: source)
self.auth = .init()
}
}
struct AuthenticatedContext: ChildRequestContext {
typealias ParentContext = AppRequestContext
var coreContext: CoreRequestContextStorage
var user: User
init(context: AppRequestContext) throws {
self.coreContext = context.coreContext
self.user = try context.auth.require(User.self)
}
}
let router = Router(context: AppRequestContext.self)
router.group("api")
.add(middleware: BearerAuthenticator())
.group(context: AuthenticatedContext.self) { protected in
protected.get("/me") { _, context -> User in
context.user
}
}
See references/request-context.md for detailed patterns including multi-level child contexts and protocol composition.
Route Handlers
Handlers receive Request and Context, returning any ResponseGenerator:
router.get("/hello") { _, _ in
"Hello, World!"
}
router.get("/health") { _, _ -> HTTPResponse.Status in
.ok
}
router.get("/user") { _, _ -> User in
User(id: 1, name: "Alice")
}
router.get("/custom") { _, _ -> Response in
Response(
status: .ok,
headers: [.contentType: "text/plain"],
body: .init(byteBuffer: ByteBuffer(string: "Custom response"))
)
}
Request Handling
Path Parameters
router.get("/users/{id}") { request, context in
let id = context.parameters.get("id")!
return "User ID: \(id)"
}
router.get("/posts/{postId}/comments/{commentId}") { request, context in
let postId = context.parameters.get("postId")!
let commentId = context.parameters.get("commentId")!
return "Post \(postId), Comment \(commentId)"
}
Query Parameters
router.get("/search") { request, context in
let query = request.uri.queryParameters.get("q") ?? ""
let page = request.uri.queryParameters.get("page").flatMap(Int.init) ?? 1
return "Searching for '\(query)' on page \(page)"
}
Request Body (JSON)
struct CreateUserRequest: Decodable {
let name: String
let email: String
}
router.post("/users") { request, context in
let input = try await request.decode(as: CreateUserRequest.self, context: context)
return HTTPResponse.Status.created
}
Headers
router.get("/protected") { request, context in
guard let auth = request.headers[.authorization] else {
throw HTTPError(.unauthorized)
}
return "Authorized"
}
Response Handling
ResponseCodable
Types conforming to ResponseCodable auto-encode to JSON:
struct User: ResponseCodable {
let id: Int
let name: String
let email: String
}
router.get("/users/{id}") { request, context -> User in
return User(id: 1, name: "Alice", email: "alice@example.com")
}
EditedResponse
Control status code and headers with response body:
router.post("/users") { request, context -> EditedResponse<User> in
let user = User(id: 1, name: "Alice", email: "alice@example.com")
return EditedResponse(
status: .created,
headers: [.location: "/users/1"],
response: user
)
}
HTTPError
Throw errors for HTTP error responses:
router.get("/users/{id}") { request, context in
guard let user = findUser(id: context.parameters.get("id")!) else {
throw HTTPError(.notFound, message: "User not found")
}
return user
}
Middleware
Middleware processes requests before handlers and responses after:
struct LoggingMiddleware<Context: RequestContext>: RouterMiddleware {
func handle(
_ request: Request,
context: Context,
next: (Request, Context) async throws -> Response
) async throws -> Response {
let start = ContinuousClock.now
let response = try await next(request, context)
let duration = ContinuousClock.now - start
print("\(request.method) \(request.uri.path) - \(response.status) (\(duration))")
return response
}
}
router.middlewares.add(LoggingMiddleware())
router.group("api") { api in
api.middlewares.add(AuthMiddleware())
api.get("/protected") { _, _ in "Secret data" }
}
Built-in Middleware
import Hummingbird
router.middlewares.add(CORSMiddleware(
allowOrigin: .originBased,
allowHeaders: [.contentType, .authorization],
allowMethods: [.get, .post, .put, .delete]
))
router.middlewares.add(FileMiddleware(rootFolder: "public"))
router.middlewares.add(MetricsMiddleware())
router.middlewares.add(TracingMiddleware())
Application Configuration
let app = Application(
router: router,
configuration: .init(
address: .hostname("0.0.0.0", port: 8080),
serverName: "MyApp"
)
)
try await app.runService()
With ServiceLifecycle
import Hummingbird
import ServiceLifecycle
@main
struct App {
static func main() async throws {
let router = Router()
let app = Application(
router: router,
configuration: .init(address: .hostname("0.0.0.0", port: 8080))
)
let serviceGroup = ServiceGroup(
services: [app],
gracefulShutdownSignals: [.sigterm, .sigint],
logger: Logger(label: "app")
)
try await serviceGroup.run()
}
}
Extensions
Authentication (HummingbirdAuth)
.product(name: "HummingbirdAuth", package: "hummingbird-auth")
import HummingbirdAuth
struct AppRequestContext: AuthRequestContext {
var coreContext: CoreRequestContextStorage
var auth: LoginCache
init(source: Source) {
self.coreContext = .init(source: source)
self.auth = .init()
}
}
router.group("api")
.add(middleware: BearerAuthenticator())
.get("/me") { request, context -> User in
let user = try context.auth.require(User.self)
return user
}
WebSockets (HummingbirdWebSocket)
.product(name: "HummingbirdWebSocket", package: "hummingbird-websocket")
import HummingbirdWebSocket
router.ws("/chat") { inbound, outbound, context in
for try await message in inbound {
switch message {
case .text(let text):
try await outbound.write(.text("Echo: \(text)"))
case .binary(let data):
try await outbound.write(.binary(data))
}
}
}
Fluent ORM (HummingbirdFluent)
.product(name: "HummingbirdFluent", package: "hummingbird-fluent")
import HummingbirdFluent
import FluentPostgresDriver
let fluent = Fluent(logger: logger)
fluent.databases.use(.postgres(configuration: postgresConfig), as: .psql)
let app = Application(
router: router,
configuration: .init(address: .hostname("0.0.0.0", port: 8080))
)
app.addServices(fluent)
HTTP/2 and TLS
.product(name: "HummingbirdHTTP2", package: "hummingbird")
.product(name: "HummingbirdTLS", package: "hummingbird")
Testing
import HummingbirdTesting
import Testing
@Test func testHealthEndpoint() async throws {
let router = Router()
router.get("/health") { _, _ -> HTTPResponse.Status in .ok }
let app = Application(router: router)
try await app.test(.router) { client in
try await client.execute(uri: "/health", method: .get) { response in
#expect(response.status == .ok)
}
}
}
@Test func testCreateUser() async throws {
let app = buildApplication()
try await app.test(.router) { client in
let user = CreateUserRequest(name: "Alice", email: "alice@example.com")
try await client.execute(
uri: "/users",
method: .post,
headers: [.contentType: "application/json"],
body: JSONEncoder().encodeAsByteBuffer(user, allocator: .init())
) { response in
#expect(response.status == .created)
}
}
}
Best Practices
1. Use Custom Request Contexts
Extend contexts for type-safe access to authentication, database connections, etc:
struct AppRequestContext: AuthRequestContext {
var coreContext: CoreRequestContextStorage
var auth: LoginCache
init(source: Source) {
self.coreContext = .init(source: source)
self.auth = .init()
}
}
2. Organize Routes
Split routes into separate files/functions:
func addUserRoutes(to router: Router<AppRequestContext>) {
router.group("users") { users in
users.get { _, _ in }
users.post { _, _ in }
users.get("{id}") { _, _ in }
}
}
let router = Router(context: AppRequestContext.self)
addUserRoutes(to: router)
addPostRoutes(to: router)
3. Use Middleware for Cross-Cutting Concerns
Apply authentication, logging, and metrics via middleware rather than in handlers.
4. Handle Errors Gracefully
router.get("/users/{id}") { request, context in
guard let id = context.parameters.get("id"),
let user = try await userService.find(id: id) else {
throw HTTPError(.notFound, message: "User not found")
}
return user
}
Reference Files
Load these files as needed for specific topics:
references/request-context.md - RequestContext protocol, ChildRequestContext, protocol composition, AuthRequestContext, multi-level contexts
references/routing.md - Advanced routing patterns, result builder router, wildcards
references/middleware.md - Custom middleware, authentication, CORS, file serving
references/testing.md - Testing strategies, mocking, integration tests