Warning
This is experimental project don't use in production.
A batteries-included, function-based Swift server framework inspired by http4k, focused on building services and APIs.
- 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
/docscovering 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
TestClientfor fast integration tests
This project uses swiftly. Install it, then run:
swiftly install 6.3.3
swiftly use 6.3.3The .swift-version file pins the toolchain, so swift commands inside the repo automatically use it.
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 testKnown issue: with the Xcode 26 beta SDK (macOS 27),
swift-crypto4.5.1 fails to build due to a newContiguousBytes.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.
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
containerinstalled (container system start)- No other service on host ports
8080/8081
make linux-buildThis creates a swift:6.3.3 container, mounts the repo at /src, and runs swift build.
make linux-runBuilds 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.
make linux-testmake linux-cleanmake 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 artifactsRun 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
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)")
}),
]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)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/EchoThe sample gRPC client accepts a token via environment:
GRPC_TOKEN=<token> swift run GRPCProtoClientConduit 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 logsmetricsMiddleware()— request/error counts and durations via swift-metricssimpleMetricsMiddleware(metrics:)+metricsRoute(metrics:)— lightweight in-memory/metricsJSON endpoint
let app = compose([
loggingMiddleware(logger: logger),
metricsMiddleware(),
corsMiddleware(.permissive), // or CORSConfiguration(allowedOrigins: [...])
problemDetailsMiddleware(), // RFC 7807 errors
validationMiddleware([.maxBodySize(1024)]),
gzipMiddleware(),
], trieRouter(routes))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),
]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)# 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"