Frameworks

Generate an SDK from Hono with @hono/zod-openapi

Hono itself has no OpenAPI output — it's a router, not a spec generator. @hono/zod-openapi is the standard fix: an extended Hono class where you define routes with Zod schemas, and the same schemas that validate requests at runtime also produce a real OpenAPI document, served from your app at a path you choose. From there, a Hono OpenAPI SDK is the same two-step problem as any other framework — get a clean spec out, then run it through a generator or a hosted pipeline like Sourced.

Getting an OpenAPI spec out of Hono

@hono/zod-openapi wraps Zod to OpenAPI and exposes three building blocks: createRoute to describe an operation, OpenAPIHono in place of Hono, and .doc() to serve the generated document.

Step Code What it does
1. Install npm i hono zod @hono/zod-openapi Adds the package alongside its hono and zod peer deps
2. Define schemas z.object({...}).openapi('User') The z import from @hono/zod-openapi tags schemas for the spec; .openapi('Name') registers a named, reusable components/schemas entry
3. Describe a route createRoute({ method, path, request, responses }) Builds one OpenAPI operation object
4. Register the app new OpenAPIHono(), then app.openapi(route, handler) Wires the route to a typed handler with request validation
5. Serve the spec app.doc('/doc', { openapi: '3.0.0', info: {...} }) Serves the generated document as JSON at the path you name
import { createRoute, z, OpenAPIHono } from '@hono/zod-openapi'

const UserSchema = z.object({
  id: z.string().openapi({ example: '123' }),
  name: z.string().openapi({ example: 'John Doe' }),
}).openapi('User')

const route = createRoute({
  method: 'get',
  path: '/users/{id}',
  request: { params: z.object({ id: z.string().openapi({ param: { name: 'id', in: 'path' } }) }) },
  responses: {
    200: { content: { 'application/json': { schema: UserSchema } }, description: 'Retrieve the user' },
  },
})

const app = new OpenAPIHono()
app.openapi(route, (c) => c.json({ id: '123', name: 'John Doe' }, 200))
app.doc('/doc', { openapi: '3.0.0', info: { version: '1.0.0', title: 'My API' } })

OpenAPI 3.1 output

.doc() defaults to whatever openapi version string you pass it, including 3.0.0 in the docs' own example. For 3.1 specifically, use app.doc31('/docs', { openapi: '3.1.0', info: {...} }) (or the underlying app.getOpenAPI31Document(...) method) — a separate endpoint from .doc(), not a flag on it.

Gotcha 1: operationId is optional, not automatic

createRoute's config type extends the standard OpenAPI OperationObject (via zod-to-openapi's RouteConfig), which means operationId is a valid field — but nothing requires you to set it. Leave it off, and the field is simply absent from that operation, which is spec-legal but leaves a downstream SDK generator to invent a method name from the HTTP verb and path. See how to fix missing operationId errors for what that fallback looks like, and add operationId to createRoute() for any endpoint you expect customers to call by name.

Gotcha 2: schemas need .openapi('Name') or they end up anonymous

A Zod schema only becomes a named, reusable components/schemas entry when you tag it with .openapi('Name'). Skip that call and pass the raw schema inline to responses or request.body, and the generated spec gets an unnamed inline schema instead — the same shape that produces generator-invented names like InlineObject or Type1 downstream. See fixing inline schema names for what that looks like once it reaches SDK generation, and reuse the same tagged schema across routes rather than redefining it inline each time.

From a Hono spec to hosted docs and typed SDKs

Once /doc serves a clean spec — named schemas, explicit operationIds — turning it into an installable SDK is a separate step. With Sourced:

  1. Point Sourced at your repo or the served spec. Connect from GitHub, or fetch /doc from a running instance and upload the JSON.
  2. Preview the TypeScript and Python SDKs. Method names and typed models render before anything publishes, so a missing operationId or an anonymous schema is visible in preview, not a customer's autocomplete.
  3. Get hosted docs and an llms.txt from the same pass. No separate docs deploy — the hosted docs preview and llms.txt come from the same spec.
  4. Publish on your schedule. Free unlimited previews and up to 2 hosted noindex docs review sites and one hosted MCP server, no credit card, cover steps 1-3.

Comparison: @hono/zod-openapi vs. a full SDK pipeline

Need @hono/zod-openapi gives you What it doesn't give you
A generated OpenAPI spec Yes, from the same Zod schemas that validate requests Nothing — this part is real and free
Request validation synced with docs Yes — one schema drives both Nothing to add here
Interactive API browsing The raw JSON at your .doc() path A hosted docs site for customers
A typed client SDK Nothing built in Generated TypeScript or Python SDK
A diff before a breaking release Nothing built in A compatibility report
Agent-readable docs Nothing built in llms.txt derived from the spec

OSS generators for other languages

Sourced's generated SDKs focus on TypeScript and Python today — the two ecosystems where teams publish first, and where Hono itself lives. For every other target, a clean @hono/zod-openapi spec works with the standard per-language OpenAPI Generator ecosystem — Go, Java, Ruby, PHP, C#, Kotlin, Rust, and Swift all have their own walkthroughs. The five-minute generate-an-SDK guide covers running a local generator against any spec.

Other backend frameworks with their own spec-extraction paths: FastAPI, Rails, NestJS, Express, Django, Spring Boot, Laravel, and .NET.

Honest scope: when @hono/zod-openapi alone is enough

If your Hono API only has internal TypeScript consumers, importing the AppType directly with Hono's RPC client is less work than generating a full SDK — you get end-to-end types without a spec at all. @hono/zod-openapi earns its place once you need REST for non-TypeScript consumers, a docs site, or a spec that other tooling (mocking, contract testing, a gateway) can read. Reach for full SDK generation on top of it once you have external customers or a second language to support.

FAQ

Does Hono generate OpenAPI automatically?

No. Plain Hono has no OpenAPI output. @hono/zod-openapi is the standard add-on — it's an extended Hono class that builds a spec from the same Zod schemas you use for request validation.

Where does @hono/zod-openapi serve the spec?

Wherever you call .doc() — for example app.doc('/doc', { openapi: '3.0.0', info: {...} }) serves it at /doc on the running app. There's no fixed default path; you name it.

Does @hono/zod-openapi support OpenAPI 3.1?

Yes, via a separate method: app.doc31('/docs', { openapi: '3.1.0', info: {...} }), or the underlying app.getOpenAPI31Document(...). The plain .doc() method is not itself version-locked, but its own example defaults to 3.0.0.

Why is my generated SDK method named something generic instead of the name I expected?

That's the fallback when operationId is missing from a createRoute() config. The field is supported — RouteConfig extends the standard OpenAPI OperationObject — but it's optional, so leaving it off lets a downstream generator invent a name from the verb and path.

Why does my response schema show up as InlineObject or Type1 in the generated SDK?

The Zod schema wasn't tagged with .openapi('Name') before being used in a route's request or responses. Untagged schemas stay inline in the generated spec instead of becoming a named components/schemas entry, and generators invent a name for any unnamed inline schema.

Do I need @hono/zod-openapi if all my consumers are also TypeScript?

Not necessarily. Hono's own RPC client gives you end-to-end type inference from AppType without a spec in the loop. Reach for @hono/zod-openapi once you need REST for non-TypeScript clients or a docs site.

Ship it

Once .doc() serves a spec with named schemas and explicit operationIds, you have a real Hono OpenAPI SDK source to work with. Start free on Sourced to preview a TypeScript and Python SDK plus hosted docs from that spec in one pass, or run it through the OpenAPI validator first to catch gaps before you generate anything.