Skip to content

Server interface

@codewithagents/openapi-server reads your OpenAPI 3.x spec and writes a typed service interface into your project. The interface is plain TypeScript with no framework imports: implement it however you want and wire it to Hono, Express, Fastify, or any router you already use. Set "framework": "hono" (or "express" / "fastify") and the generator also scaffolds a ready-to-mount router, with optional Zod validation at the boundary so invalid requests never reach your service implementation.

Running the generator produces up to two files in your output directory:

FileContents
service.tsTypeScript interface with one typed async method per API operation. No framework imports.
router.tsRouter factory, generated only when framework is set to "hono", "express", or "fastify"

Key guarantees:

  • Framework-agnostic service interface. service.ts has zero framework dependencies. The TypeScript compiler enforces the contract: add an endpoint in the spec and forget to implement it, and the build fails.
  • Optional router scaffolding. Choose "hono", "express", or "fastify" to get a ready-to-mount router as a starting point. Use "none" to wire the interface yourself with any framework.
  • Supports GET, POST, PUT, PATCH, DELETE. All five HTTP methods are generated when present in the spec.
  • Zod validation at the boundary. Point input_schema at the same Zod schema file you use with @codewithagents/openapi-gen. The generator adds safeParse calls to every route that receives a body, returning a structured 422 before the call ever reaches your service.
  • Prettier-clean output. Every generated file passes prettier --check with your project’s own Prettier config.
  • strict: true. All output compiles cleanly with TypeScript strict mode.
  • OpenAPI 3.1.x is the primary target (including 3.1.1). OpenAPI 3.0.x is best-effort. Full support for $ref, allOf, anyOf, oneOf, and nullable.
Terminal window
npm i -D @codewithagents/openapi-server

1. Create openapi-server.config.json in your project root:

{
"input_openapi": "./spec/api.json",
"output": "./generated",
"framework": "hono"
}

2. Run both generators:

Terminal window
npx openapi-gen && npx openapi-server

Or add a combined script to package.json:

{
"scripts": {
"generate": "openapi-gen && openapi-server"
}
}

3. Implement the generated service interface:

src/server/petService.ts
import { randomUUID } from 'node:crypto'
import type { PetstoreService } from '../generated/service.js'
import type { Pet } from '../generated/models.js'
const pets = new Map<string, Pet>()
export const petService: PetstoreService = {
async listPets(params) {
const all = Array.from(pets.values())
if (params?.species) {
return all.filter((p) => p.species.toLowerCase() === params.species!.toLowerCase())
}
return all
},
async createPet(body) {
const pet: Pet = { id: randomUUID(), ...body }
pets.set(pet.id, pet)
return pet
},
async getPet(id) {
const pet = pets.get(id)
if (!pet) throw new Error(`Pet ${id} not found`)
return pet
},
async deletePet(id) {
pets.delete(id)
},
}

4. Mount the generated router and serve:

src/server/index.ts
import { serve } from '@hono/node-server'
import { Hono } from 'hono'
import { createRouter } from '../generated/router.js'
import { petService } from './petService.js'
const app = new Hono()
// Mount API routes at /api
const apiRouter = createRouter(petService)
app.route('/api', apiRouter)
serve({ fetch: app.fetch, port: 3001 })

The config file must be valid JSON and must have a .json extension. By default the generator looks for openapi-server.config.json in the current working directory. Use --config <path> to point at a different file.

Terminal window
npx openapi-server --config ./config/openapi-server.config.json
FieldTypeRequiredDefaultDescription
input_openapistringYesn/aPath to your OpenAPI spec (JSON or YAML)
outputstringYesn/aDirectory to write the generated files
framework"hono" | "express" | "fastify" | "none"No"none"Router framework to generate. Use "none" to generate only service.ts
input_schemastringNon/aPath to user-owned Zod schema file. Enables server-side request validation (never overwritten)

Full config example:

{
"input_openapi": "./spec/api.json",
"output": "./generated",
"framework": "hono",
"input_schema": "./generated/schemas.ts"
}

Given a petstore spec (GET /pets, POST /pets, GET /pets/{id}, DELETE /pets/{id}), the generator produces a plain TypeScript interface with no framework dependencies:

// generated/service.ts (auto-generated)
import type { CreatePetRequest, Pet } from './models.js'
export interface PetstoreService {
/** GET /pets */
listPets(params?: { species?: string; limit?: number }): Promise<Pet[]>
/** POST /pets */
createPet(body: CreatePetRequest): Promise<Pet>
/** GET /pets/{id} */
getPet(id: string): Promise<Pet>
/** DELETE /pets/{id} */
deletePet(id: string): Promise<void>
}

The interface is regenerated every time the spec changes. If you add an endpoint in the spec and forget to implement it, TypeScript will fail the build.

Method name derivation: method names come from operationId in your spec (sanitized to camelCase). When operationId is absent, the generator falls back to a verb + path heuristic: GET /pets/{id} becomes getPetsById, POST /users becomes createUsers. Any leading /api/v{n}/ prefix (for example /api/v1/) is stripped before this heuristic runs, so /api/v1/pets/{id} and /pets/{id} both produce getPetsById.

createRouter(service) returns a Hono instance. Mount it anywhere in your app:

// generated/router.ts (auto-generated, framework: "hono")
import { Hono } from 'hono'
import type { CreatePetRequest } from './models.js'
import type { PetstoreService } from './service.js'
export function createRouter(service: PetstoreService): Hono {
const app = new Hono()
app.get('/pets', async (c) => {
const params = {
species: c.req.query('species') ?? undefined,
limit: c.req.query('limit') !== undefined ? Number(c.req.query('limit')) : undefined,
}
return c.json(await service.listPets(params))
})
app.post('/pets', async (c) => {
const body = await c.req.json<CreatePetRequest>()
return c.json(await service.createPet(body), 201)
})
app.get('/pets/:id', async (c) => {
return c.json(await service.getPet(c.req.param('id')))
})
app.delete('/pets/:id', async (c) => {
await service.deletePet(c.req.param('id'))
return new Response(null, { status: 204 })
})
return app
}

The router handles:

  • Path params: {id} becomes :id, extracted via the framework’s native param accessor
  • Query params: extracted and typed (string, number, boolean)
  • Request bodies: parsed and cast to the correct model type
  • Response status: spec-driven. The generator reads your spec’s response codes first (checking for 201, then 204, then 200). When no explicit code is declared, it falls back to 204 for DELETE and 200 for all other methods.

createRouter returns a plain Hono instance. Mount it at any path prefix, add middleware, or nest it inside a larger app:

src/server/index.ts
import { serve } from '@hono/node-server'
import { Hono } from 'hono'
import { createRouter } from '../generated/router.js'
import { petService } from './petService.js'
const app = new Hono()
app.route('/api', createRouter(petService))
serve({ fetch: app.fetch, port: 3001 })

Set "framework": "express" and the generator produces a createRouter that returns an Express Router. Apply express.json() middleware before mounting so req.body is populated:

src/server/index.ts
import express from 'express'
import { createRouter } from '../generated/router.js'
import { petService } from './petService.js'
const app = express()
app.use(express.json())
app.use('/api', createRouter(petService))
app.listen(3001)

Use "none" when you want to own the routing layer entirely. The generator produces only service.ts and adds no runtime dependencies:

{
"input_openapi": "./spec/api.json",
"output": "./generated",
"framework": "none"
}

Implement the interface and wire it to any router manually:

import { createServer } from 'node:http'
import type { PetstoreService } from '../generated/service.js'
import { petService } from './petService.js'
// Wire PetstoreService however you like: vanilla http, Koa, Bun, Deno, or anything else.

Point input_schema at the same schemas.ts you use with @codewithagents/openapi-gen. The generator adds safeParse calls to every route that receives a request body. Invalid requests get a structured 422 response and never reach your service implementation.

Config:

{
"input_openapi": "./spec/api.json",
"output": "./generated",
"framework": "hono",
"input_schema": "./generated/schemas.ts"
}

Generated router with validation (two-pass output):

app.post('/pets', async (c) => {
const body = await c.req.json<CreatePetRequest>()
// Validate request body: returns 422 with Zod issues on failure
const parseResult = CreatePetRequestSchema.safeParse(body)
if (!parseResult.success) {
return c.json({ error: 'Invalid request body', issues: parseResult.error.issues }, 422)
}
const validatedBody = parseResult.data
return c.json(await service.createPet(validatedBody), 201)
})

Invalid requests receive a structured 422 response:

{
"error": "Invalid request body",
"issues": [{ "code": "too_small", "path": ["name"], "message": "Name is required" }]
}

The generator runs in two passes: first it writes router.ts without any Zod imports, then it rewrites router.ts with safeParse calls for every route whose body type has a matching schema in input_schema. If the schema file does not exist yet at generation time, the second pass is skipped and validation is added on the next run.

The generated router is a plain framework object. Add any middleware before mounting it:

import { Hono } from 'hono'
import { bearerAuth } from 'hono/bearer-auth'
import { cors } from 'hono/cors'
import { createRouter } from '../generated/router.js'
import { petService } from './petService.js'
const app = new Hono()
// Auth and CORS apply to all /api routes
app.use('/api/*', cors())
app.use('/api/*', bearerAuth({ token: process.env.API_TOKEN! }))
app.route('/api', createRouter(petService))

The generated router does not wrap service calls in try/catch. If your service method throws, the error propagates to the framework’s own error handler, which typically returns 500. Add a custom error handler to control the response shape:

import { Hono } from 'hono'
import { createRouter } from '../generated/router.js'
import { petService } from './petService.js'
const app = new Hono()
app.route('/api', createRouter(petService))
app.onError((err, c) => {
console.error(err)
return c.json({ error: err.message }, 500)
})

The generator intentionally omits several OpenAPI features. These are not validated or generated:

FeatureStatus
Response-body validationNot generated. Validate in your service if needed.
Request headers and cookiesNot extracted or validated.
Security scheme enforcementNot generated. Add auth middleware manually (see above).
multipart/form-data bodiesNot generated. Wire file upload routes manually.
Path parameter validationNot generated. Path params are passed to your service as raw strings. Validate them in your service implementation.

Requests are not validated; invalid bodies reach my service. Check that input_schema is set in your config and the schema file exists. If the schema file is missing at generation time, the Zod pass is skipped and validation is not added. Also confirm that the schema name matches the convention: for a body type CreatePetRequest, the schema must be exported as CreatePetRequestSchema. A mismatch triggers a warning to stderr.

Cannot find module './models.js' at runtime. service.ts imports from models.ts which is generated by @codewithagents/openapi-gen. Run openapi-gen before openapi-server. A combined script ("generate": "openapi-gen && openapi-server") prevents this.

My service throws but the client sees 500 with no details. The generated router does not catch errors. Add a framework error handler as shown in the Error handling section above.

Types and fetch client

@codewithagents/openapi-gen generates the models.ts that service.ts imports from. Run both generators together against the same spec.

Learn more

React Query hooks

Add @codewithagents/openapi-react-query to generate typed useQuery and useMutation hooks for the same spec your server implements.

Learn more

Form error mapping

Add @codewithagents/api-errors to map API error responses from the generated client directly to form field errors.

Learn more