OpenAPI@Swift
·
6 min read
tl;dr: A contract-first walkthrough that pairs OpenAPI with Hono on Workers and Swift OpenAPI Generator to keep clients and servers in sync.
WWDC23: Meet Swift OpenAPI Generator
OpenAPI
OpenAPI is a contract for describing HTTP services. You write it in YAML or JSON, and tools read it to automate workflows like generating the code to send and receive requests.
+--------------------+
| OpenAPI spec | (openapi.yaml/json)
+----------+---------+
|
+----------v-----------+
| |
+----------+-----------+
| Client | Type-safe | Server stub |
| (Swift, | calls | (Vapor, etc) |
| Kotlin, |<----------+ |
| TS...) | | |
+----------+ +--------------+
| |
| +-------v-------+
| | Tests / Docs |
+-->| (Swagger UI) |
+---------------+
Core ideas
- Contract-first: keep the API contract in a shared spec file owned by frontend and backend together.
- Schema + Operation: Schemas define data structures; operations define HTTP methods, paths, request bodies, and responses.
- Tooling ecosystem: Swagger UI and ReDoc for docs; generators for clients/servers; CI for contract validation.
OpenAPI’s value is its standardized, machine-readable API description: it improves collaboration and lets tooling automate large chunks of the API lifecycle, boosting efficiency and consistency.
Example
openapi: 3.0.3
info:
title: Todo API
version: 1.0.0
paths:
/todos:
get:
summary: List all todos
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/TodoList'
components:
schemas:
TodoList:
type: object
properties:
todos:
type: array
items:
$ref: '#/components/schemas/Todo'
Todo:
type: object
required: [id, title]
properties:
id:
type: string
title:
type: string
Compute@Edge
Hono
Hono is a TypeScript web framework tuned for edge platforms, aiming for “ready in seconds, responds in milliseconds.” It runs on Cloudflare Workers, Deno, Bun, and more: think a mini Express that keeps edge-friendly cold starts and tiny bundles.
┌──────────────────────┐
│ OpenAPI Contract │ ← shared API spec (YAML/JSON)
└──────────┬───────────┘
│ export / import
▼
┌──────────────────────┐ ┌─────────────────────┐
│ @hono/zod-openapi │ ---> │ Hono Routes │
│ (emit/validate schema) │ (Cloudflare Worker) │
└──────────┬───────────┘ └──────────┬──────────┘
│ │
│ Swift OpenAPI Generator │
▼ ▼
Swift client / server stubs Edge API runtime
A minimal example of Hono inside a Worker:
import { Hono } from 'hono'
const app = new Hono()
app.get('/hello', (c) => c.json({ message: 'Hello from Hono on Workers!' }))
export default app // fetch entrypoint for Cloudflare Workers
To combine Hono with OpenAPI, use @hono/zod-openapi (or @hono/openapi) to declare Zod schemas for requests and responses. The framework validates requests at runtime and emits a standard OpenAPI document that your Swift generator can consume.
import { Hono } from 'hono'
import { z } from 'zod'
import { OpenAPI, createRoute, zValidator } from '@hono/zod-openapi'
const app = new Hono<{ Variables: { openapi: OpenAPI } }>()
app.use('*', OpenAPI())
const listTodosRoute = createRoute({
method: 'get',
path: '/todos',
responses: {
200: {
description: 'Todo List',
content: {
'application/json': {
schema: z.object({
todos: z.array(
z.object({
id: z.string(),
title: z.string()
})
)
})
}
}
}
}
})
app.openapi(listTodosRoute, async (c) => {
const todos = await c.env.DB.listTodos()
return c.json({ todos })
})
// Export inside a Cloudflare Worker
export default app
export const onRequestGet = app.fetch // Optional: for Pages Functions compatibility
// Export the OpenAPI document (e.g., /openapi.json)
app.doc('/openapi.json', {
openapi: '3.0.3',
info: { title: 'Todo API', version: '1.0.0' }
})
This setup gives you:
- Contracts declared at the routing layer, avoiding drift between docs and implementation.
wrangler devserving both APIs and the OpenAPI doc locally.swift-openapi-generatorconsuming/openapi.jsonto emit a type-safe client.- Contract changes surfaced at build time by both Hono routes and the Swift compiler, shortening the debugging loop.
Swift
Swift OpenAPI Generator is a Swift package plugin that emits the boilerplate needed to call APIs or implement an API server.
It generates code at build time, keeping it in sync with the OpenAPI document without committing artifacts to your repo.
The core idea is “build-time generation”: during swift build or an Xcode build, the plugin reads your openapi.yaml/openapi.json and config file, emits the Swift modules, and hands them to the compiler.
┌────────────────┐
│ openapi.yaml │ ← maintained API contract
└───────┬────────┘
│ read
▼
┌───────────────────────────────┐
│ OpenAPIGenerator (SPM plugin) │
│ + openapi-generator-config │
└───────┬───────────────┬───────┘
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ Client code │ │ Server stubs │
│ (Swift APIs) │ │ (Protocols) │
└───────┬──────┘ └────┬─────────┘
│ │
compiled into app compiled into backend
Configuration
See: Documentation
generate:
- types
- client
namingStrategy: idiomatic
Client
import OpenAPIRuntime
import OpenAPIURLSession
import Foundation
// Instantiate your chosen transport library.
let transport: ClientTransport = URLSessionTransport()
// Create a client to connect to a server URL documented in the OpenAPI document.
let client = Client(
serverURL: try Servers.server1(),
transport: transport
)
// Make the HTTP call using a type-safe method.
let response = try await client.getGreeting(.init(query: .init(name: "Jane")))
// Switch over the HTTP response status code.
switch response {
case .ok(let okResponse):
// Switch over the response content type.
switch okResponse.body {
case .json(let greeting):
// Print the greeting message.
print("👋 \(greeting.message)")
}
case .undocumented(statusCode: let statusCode, _):
// Handle HTTP response status codes not documented in the OpenAPI document.
print("🥺 undocumented response: \(statusCode)")
}
Package ecosystem
The Swift OpenAPI Generator project is split across multiple repositories to enable extensibility and minimize dependencies in your project.
| Repository | Description |
|---|---|
| apple/swift-openapi-generator | Swift package plugin and CLI |
| apple/swift-openapi-runtime | Runtime library used by the generated code |
| apple/swift-openapi-urlsession | ClientTransport using URLSession |
| swift-server/swift-openapi-async-http-client | ClientTransport using AsyncHTTPClient |
| vapor/swift-openapi-vapor | ServerTransport using Vapor |
| hummingbird-project/swift-openapi-hummingbird | ServerTransport using Hummingbird |
| awslabs/swift-openapi-lambda | ServerTransport using AWS Lambda |
Known Issues
On Swift 6, to keep concurrency checks intact, set the following in your Xcode project:
Swift Compiler - Concurrency
| Setting | < Xcode 26 | Xcode 26 |
|---|---|---|
| Approachable Concurrency | No | Yes |
| Default Actor Isolation | nonisolated | MainActor |