Skip to content

Repository files navigation

Warning

This is experimental project don't use in production.

logo

Conduit

A batteries-included, function-based Swift server framework inspired by http4k, focused on building services and APIs.

Features

  • HTTP/1.1 + gRPC servers on SwiftNIO (HTTP/2 for gRPC)
  • JWT authentication for REST and gRPC (HMAC, EdDSA, JWKS) via jwt-kit
  • OpenTelemetry observability (logs, metrics, traces) via swift-otel
  • OpenAPI 3.0 documentation auto-generated from route metadata, with embedded Swagger UI
  • gRPC Server Reflection for tooling (grpcurl, Postman, etc.)
  • Unified docs portal at /docs covering REST + gRPC
  • CORS, gzip, validation, RFC 7807 problem details middlewares
  • Type-safe endpoints with Codable JSON and SwiftProtobuf
  • Query parameters, path params, wildcard routes, route grouping
  • Static file serving with streaming and path-traversal protection
  • In-process TestClient for fast integration tests

Toolchain

This project uses swiftly. Install it, then run:

swiftly install 6.3.3
swiftly use 6.3.3

The .swift-version file pins the toolchain, so swift commands inside the repo automatically use it.

macOS note

On macOS, swift test requires an Xcode toolchain because the open-source toolchain does not ship XCTest:

make test
# or explicitly:
DEVELOPER_DIR=/Applications/Xcode.app swift test

Known issue: with the Xcode 26 beta SDK (macOS 27), swift-crypto 4.5.1 fails to build due to a new ContiguousBytes.withBytes(RawSpan) requirement. This is an upstream incompatibility; use the open-source toolchain for builds until swift-crypto ships a fix, or a newer Xcode/SwiftCrypto release.

swift build and swift run work fine with the open-source toolchain.

Linux validation with Apple container

To make sure the project compiles and runs on Linux from a Mac, this repo includes targets that use Apple's container tool (a lightweight VM-based Linux container runtime for Apple silicon).

Requirements:

  • macOS 26 or later
  • Apple Silicon
  • container installed (container system start)
  • No other service on host ports 8080 / 8081

Build on Linux

make linux-build

This creates a swift:6.3.3 container, mounts the repo at /src, and runs swift build.

Run the sample on Linux

make linux-run

Builds the project inside the container, then starts ConduitSample in a new container and forwards ports 8080 (HTTP) and 8081 (gRPC) to the host.

Verify from inside the container:

container exec conduit-run bash -c 'cat > /tmp/check.swift <<"EOF"
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
let url = URL(string: "http://127.0.0.1:8080/health")!
let semaphore = DispatchSemaphore(value: 0)
URLSession.shared.dataTask(with: url) { data, _, _ in
    print(data.flatMap { String(data: $0, encoding: .utf8) } ?? "")
    semaphore.signal()
}.resume()
semaphore.wait()
EOF
swift /tmp/check.swift'

Expected output: ok.

Run tests on Linux

make linux-test

Clean up Linux containers

make linux-clean

Quickstart

make build           # Build the project
make test            # Run tests (Xcode toolchain on macOS)
make run             # Run the sample server
make run-grpc-client # Run the gRPC protobuf test client
make proto           # Regenerate Swift from Protos/
make clean           # Clean build artifacts

Run the sample:

swift run ConduitSample
  • HTTP: http://127.0.0.1:8080
  • gRPC: 127.0.0.1:8081
  • API docs portal: http://127.0.0.1:8080/docs
  • Swagger UI: http://127.0.0.1:8080/swagger
  • OpenAPI spec: http://127.0.0.1:8080/openapi.json

Defining APIs

import Conduit
import NIOHTTP1

let routes: [Route] = [
    route(.GET, "/health") { _ in .ok("ok") },

    route(.POST, "/echo", metadata: RouteMetadata(
        summary: "Echo a message",
        tags: ["messaging"],
        security: [.bearer],
        requestBody: .json(.object(properties: ["message": .string()])),
        responses: [200: .json(.object(properties: ["reply": .string()]))]
    ), handler: compose([
        jwtAuthMiddleware(authenticator: jwtAuth)
    ], jsonEndpoint { (req: EchoRequest, request: Request) in
        EchoResponse(reply: "Echo for \(request.userID ?? "?"): \(req.message)")
    })),

    route(.GET, "/users", metadata: RouteMetadata(
        summary: "List users with pagination",
        security: [.bearer],
        queryParameters: [
            OpenAPIParameter(name: "page", in: "query", schema: .integer()),
            OpenAPIParameter(name: "limit", in: "query", schema: .integer()),
        ],
        responses: [200: .text("User list")]
    ), handler: compose([
        jwtAuthMiddleware(authenticator: jwtAuth)
    ]) { request in
        let page = request.query["page"].flatMap(Int.init) ?? 1
        let limit = request.query["limit"].flatMap(Int.init) ?? 10
        return .ok("Users page \(page), limit \(limit)")
    }),
]

Authentication

import JWTKit

// HMAC secret (sign + verify)
let jwtAuth = await JWTAuthenticator(secret: "your-secret")

// EdDSA public key (verify only)
let jwtAuth = try await JWTAuthenticator(eddsaPublicKeyPEM: publicKeyPEM)

// JWKS from an identity provider
let jwtAuth = try await JWTAuthenticator(jwksJSON: jwks)

// Sign a token
let token = try await jwtAuth.sign(ConduitJWTPayload(
    sub: SubjectClaim(value: "user123"),
    exp: ExpirationClaim(value: Date().addingTimeInterval(3600)),
    iat: IssuedAtClaim(value: Date()),
    roles: ["user"]
))

// Protect routes
compose([jwtAuthMiddleware(authenticator: jwtAuth, requiredRoles: ["admin"])], handler)

gRPC authentication

let grpcRoutes: [GRPCRoute] = [
    GRPCRoute(path: "/echo.Echo/Echo", handler: composeGRPC([
        grpcAuthMiddleware(authenticator: jwtAuth)
    ], grpcProtoEndpoint { (req: EchoRequest) in
        EchoResponse.with { $0.reply = req.message }
    })),
    grpcReflectionRoute(for: grpcServiceRoutes),
]

Clients pass the JWT via authorization metadata:

grpcurl -plaintext \
  -H 'authorization: Bearer TOKEN' \
  -d '{"message":"hello"}' \
  127.0.0.1:8081 echo.Echo/Echo

The sample gRPC client accepts a token via environment:

GRPC_TOKEN=<token> swift run GRPCProtoClient

Observability

Conduit uses OpenTelemetry as the default observability solution:

let telemetry = try ConduitTelemetry.bootstrap(serviceName: "my-api")

try await withThrowingTaskGroup { group in
    group.addTask { try await telemetry.run() }
    group.addTask { try await HTTPServer(host: "127.0.0.1", port: 8080, handler: app).start() }
}

Configuration via standard OTEL_* environment variables (e.g. OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_LOGS_EXPORTER=console for local development).

Middleware:

  • loggingMiddleware(logger:) — structured request/response logs
  • metricsMiddleware() — request/error counts and durations via swift-metrics
  • simpleMetricsMiddleware(metrics:) + metricsRoute(metrics:) — lightweight in-memory /metrics JSON endpoint

Middleware stack

let app = compose([
    loggingMiddleware(logger: logger),
    metricsMiddleware(),
    corsMiddleware(.permissive),              // or CORSConfiguration(allowedOrigins: [...])
    problemDetailsMiddleware(),               // RFC 7807 errors
    validationMiddleware([.maxBodySize(1024)]),
    gzipMiddleware(),
], trieRouter(routes))

Documentation endpoints

Add these to your routes:

let docs: [Route] = [
    openAPIRoute("/openapi.json", title: "My API", version: "1.0.0", for: routes),
    swaggerUIRoute("/swagger", openAPIPath: "/openapi.json"),
    apiDocsPortalRoute("/docs", openAPIPath: "/openapi.json", grpcPort: 8081),
]

Testing

let client = TestClient(routes: routes)

// Unauthenticated
let res = await client.get("/health")

// With JWT
let res = await client.post("/echo", json: echo, headers: TestClient.bearerHeaders(token))

XCTAssertEqual(res.status, .ok)
let decoded = try res.decodeJSON(EchoResponse.self)

Example calls (sample app)

# Login
TOKEN=$(curl -s -X POST -H 'Content-Type: application/json' \
  -d '{"username":"admin","password":"secret"}' \
  http://127.0.0.1:8080/login | jq -r .token)

# Protected endpoint
curl -X POST -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"message":"hello"}' \
  http://127.0.0.1:8080/echo

# Query params
curl -H "Authorization: Bearer $TOKEN" "http://127.0.0.1:8080/users?page=2&limit=20"

About

Expermental Swift framework to write services

Resources

Stars

1 star

Watchers

0 watching

Forks

Contributors

Languages