mcpField
H
hummingbird

hummingbird

github.com/wendylabsinc開発6277 回インストール5 週間前に更新
リポジトリ

hummingbird について

Hummingbird は、SwiftNIO を基盤とし、Swift Concurrency に対応する Swift 向け軽量 HTTP サーバーフレームワークです。ルーティング、リクエストコンテキスト、ミドルウェア、レスポンス処理を備え、認証、WebSockets、Fluent ORM、HTTP/2、TLS に拡張できます。

主な機能

  1. 01HTTP ルーティングとルートグループ
  2. 02カスタムリクエストコンテキスト
  3. 03リクエスト処理用ミドルウェア
  4. 04JSON リクエストとレスポンス処理
  5. 05WebSockets と Fluent ORM の拡張

活用シーン

  1. 01Swift で HTTP API を構築
  2. 02ルートグループでエンドポイントを整理
  3. 03認証やログ用ミドルウェアの追加
  4. 04サーバーエンドポイントのテスト

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/wendylabsinc/claude-skills --skill hummingbird

インストール後、Claude Code で/hummingbirdを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

references/
middleware.md
request-context.md
routing.md
testing.md

詳細

Hummingbird 2

Hummingbird は、SwiftNIO を基盤とし、Swift Concurrency を全面的にサポートする、軽量で柔軟な Swift 用 HTTP サーバーフレームワークです。SSWG(Swift Server Work Group)のインキュベーションプロジェクトです。

クイックスタート

インストール

Package.swift に追加します。

dependencies: [
    .package(url: "https://github.com/hummingbird-project/hummingbird.git", from: "2.0.0")
]

ターゲットに追加します。

.target(
    name: "App",
    dependencies: [
        .product(name: "Hummingbird", package: "hummingbird")
    ]
)
最小限のアプリケーション
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()
    }
}

基本概念

ルーター

ルーターは、パスと HTTP メソッドに基づいてリクエストをハンドラーに振り分けます。

let router = Router()

// Basic routes
router.get("/users") { request, context in
    // Return all users
}

router.post("/users") { request, context in
    // Create user
}

router.get("/users/{id}") { request, context in
    let id = context.parameters.get("id")!
    // Return user by ID
}

router.put("/users/{id}") { request, context in
    // Update user
}

router.delete("/users/{id}") { request, context in
    // Delete user
}
ルートグループ

共通のプレフィックスを持つルートをまとめます。

let router = Router()

router.group("api/v1") { api in
    api.group("users") { users in
        users.get { _, _ in /* list users */ }
        users.post { _, _ in /* create user */ }
        users.get("{id}") { _, context in /* get user */ }
    }

    api.group("posts") { posts in
        posts.get { _, _ in /* list posts */ }
    }
}
リクエストコンテキスト

各リクエストにはコンテキストのインスタンスが割り当てられます。BasicRequestContext を使用するか、カスタムコンテキストを作成します。

// Using basic context
let router = Router(context: BasicRequestContext.self)

// Custom context for additional properties
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)
子リクエストコンテキスト

ChildRequestContext を使用すると、コンテキストを変換し、プロパティの存在を保証できます。

import HummingbirdAuth

// Parent context with optional authentication
struct AppRequestContext: AuthRequestContext {
    var coreContext: CoreRequestContextStorage
    var auth: LoginCache

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

// Child context guarantees authenticated user
struct AuthenticatedContext: ChildRequestContext {
    typealias ParentContext = AppRequestContext

    var coreContext: CoreRequestContextStorage
    var user: User  // Non-optional, guaranteed to exist

    init(context: AppRequestContext) throws {
        self.coreContext = context.coreContext
        self.user = try context.auth.require(User.self)
    }
}

// Use in routes
let router = Router(context: AppRequestContext.self)

router.group("api")
    .add(middleware: BearerAuthenticator())
    .group(context: AuthenticatedContext.self) { protected in
        // All routes here have guaranteed user
        protected.get("/me") { _, context -> User in
            context.user  // Non-optional access
        }
    }

複数階層の子コンテキストやプロトコル合成など、詳しいパターンについては references/request-context.md を参照してください。

ルートハンドラー

ハンドラーは Request と Context を受け取り、任意の ResponseGenerator を返します。

// Return String
router.get("/hello") { _, _ in
    "Hello, World!"
}

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

// Return Codable (auto-encoded to JSON)
router.get("/user") { _, _ -> User in
    User(id: 1, name: "Alice")
}

// Return full Response
router.get("/custom") { _, _ -> Response in
    Response(
        status: .ok,
        headers: [.contentType: "text/plain"],
        body: .init(byteBuffer: ByteBuffer(string: "Custom response"))
    )
}

リクエストの処理

パスパラメーター
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)"
}
クエリパラメーター
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)"
}
リクエストボディ(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)
    // Create user with input.name, input.email
    return HTTPResponse.Status.created
}
ヘッダー
router.get("/protected") { request, context in
    guard let auth = request.headers[.authorization] else {
        throw HTTPError(.unauthorized)
    }
    // Process authorization header
    return "Authorized"
}

レスポンスの処理

ResponseCodable

ResponseCodable に準拠する型は、自動的に 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: "[email protected]")
}
EditedResponse

レスポンスボディとともに、ステータスコードとヘッダーを制御します。

router.post("/users") { request, context -> EditedResponse<User> in
    let user = User(id: 1, name: "Alice", email: "[email protected]")
    return EditedResponse(
        status: .created,
        headers: [.location: "/users/1"],
        response: user
    )
}
HTTPError

HTTP エラーレスポンスには、エラーをスローします。

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
}

ミドルウェア

ミドルウェアは、ハンドラーの実行前にリクエストを処理し、実行後にレスポンスを処理します。

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
    }
}

// Apply to router
router.middlewares.add(LoggingMiddleware())

// Apply to route group
router.group("api") { api in
    api.middlewares.add(AuthMiddleware())
    api.get("/protected") { _, _ in "Secret data" }
}
組み込みミドルウェア
import Hummingbird

// CORS
router.middlewares.add(CORSMiddleware(
    allowOrigin: .originBased,
    allowHeaders: [.contentType, .authorization],
    allowMethods: [.get, .post, .put, .delete]
))

// File serving
router.middlewares.add(FileMiddleware(rootFolder: "public"))

// Metrics (with swift-metrics)
router.middlewares.add(MetricsMiddleware())

// Tracing (with swift-distributed-tracing)
router.middlewares.add(TracingMiddleware())

アプリケーションの設定

let app = Application(
    router: router,
    configuration: .init(
        address: .hostname("0.0.0.0", port: 8080),
        serverName: "MyApp"
    )
)

try await app.runService()
ServiceLifecycle の使用
import Hummingbird
import ServiceLifecycle

@main
struct App {
    static func main() async throws {
        let router = Router()
        // ... configure routes

        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()
    }
}

拡張機能

認証(HummingbirdAuth)
.product(name: "HummingbirdAuth", package: "hummingbird-auth")
import HummingbirdAuth

// Context with authentication
struct AppRequestContext: AuthRequestContext {
    var coreContext: CoreRequestContextStorage
    var auth: LoginCache

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

// Bearer token authentication
router.group("api")
    .add(middleware: BearerAuthenticator())
    .get("/me") { request, context -> User in
        let user = try context.auth.require(User.self)
        return user
    }
WebSocket(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

// Add Fluent to application
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 と TLS
.product(name: "HummingbirdHTTP2", package: "hummingbird")
.product(name: "HummingbirdTLS", package: "hummingbird")

テスト

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: "[email protected]")

        try await client.execute(
            uri: "/users",
            method: .post,
            headers: [.contentType: "application/json"],
            body: JSONEncoder().encodeAsByteBuffer(user, allocator: .init())
        ) { response in
            #expect(response.status == .created)
        }
    }
}

ベストプラクティス

1. カスタムリクエストコンテキストを使用する

認証やデータベース接続などに型安全にアクセスできるよう、コンテキストを拡張します。

struct AppRequestContext: AuthRequestContext {
    var coreContext: CoreRequestContextStorage
    var auth: LoginCache

    init(source: Source) {
        self.coreContext = .init(source: source)
        self.auth = .init()
    }
}
2. ルートを整理する

ルートを別々のファイルや関数に分割します。

// UserRoutes.swift
func addUserRoutes(to router: Router<AppRequestContext>) {
    router.group("users") { users in
        users.get { _, _ in /* list */ }
        users.post { _, _ in /* create */ }
        users.get("{id}") { _, _ in /* get */ }
    }
}

// Application setup
let router = Router(context: AppRequestContext.self)
addUserRoutes(to: router)
addPostRoutes(to: router)
3. 横断的な関心事にはミドルウェアを使用する

ハンドラー内ではなく、ミドルウェアを介して認証、ロギング、メトリクスを適用します。

4. エラーを適切に処理する
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
}

参考ファイル

特定のトピックに応じて、以下のファイルを読み込んでください。

  • references/request-context.md - RequestContext プロトコル、ChildRequestContext、プロトコル合成、AuthRequestContext、複数階層のコンテキスト
  • references/routing.md - 高度なルーティングパターン、result builder ルーター、ワイルドカード
  • references/middleware.md - カスタムミドルウェア、認証、CORS、ファイル配信
  • references/testing.md - テスト戦略、モック、統合テスト

コメント