All articles

OpenAPI Auto-Generation: Turn Your Code into Perfect API Docs

Stop hand-writing API documentation that drifts out of sync the moment you ship. Here is how to auto-generate OpenAPI specs directly from your code.

D

DocPilot Team

AI documentation research and engineering from Onzira Labs.

6 min read

Your API documentation is wrong. Not conceptually wrong, but factually wrong — the endpoint you shipped last Tuesday already accepts two new query parameters, the response schema gained an optional field, and the example payload still shows the old shape. Hand-written docs decay within days of the first commit, and every developer who has ever maintained a public API knows it.

OpenAPI auto-generation solves this by treating your code as the source of truth. Instead of writing YAML by hand and hoping engineers remember to update it, you generate the spec from annotations, type signatures, or runtime inspection. The result is documentation that cannot drift further than your last build.

This guide walks through the practical mechanics: which approach fits which stack, how to set up generation in a CI pipeline, and how to keep the output clean enough to publish.

What "Auto-Generation" Actually Means

The OpenAPI Initiative defines a single specification format (currently 3.1.0) for describing REST APIs. Auto-generation refers to any tooling that emits a valid OpenAPI document by reading your source code or runtime behavior, rather than by having a human author YAML.

Three generation strategies dominate:

StrategySource of TruthBest ForTrade-offs
Annotation-basedDecorators/attributes in route handlersTypeScript, Python, Java, C#Requires code changes; very rich metadata
Schema-derivedType definitions and route registrationGo, Rust, TypeScriptLight-touch but loses semantic detail
Runtime introspectionLive HTTP traffic or framework hooksAny languageCaptures reality; harder to add descriptions

Most production setups combine two. A Node.js service might use a schema-derived approach for the bulk of endpoints and add annotations only where the documentation needs richer prose.

Annotation-Based Generation in Practice

The most popular path for new projects is annotation-based, because it puts human-readable context right next to the code it describes.

TypeScript with zod-to-openapi

If you already validate request bodies with Zod, you can register each schema with a helper and produce a complete spec:

import { z } from 'zod';
import { extendZodWithOpenApi, generateDocument } from '@asteasolutions/zod-to-openapi';

extendZodWithOpenApi(z);

const UserSchema = z.object({
  id: z.string().uuid().openapi({ description: 'Server-issued UUID' }),
  email: z.string().email().openapi({ example: 'a@b.com' }),
  createdAt: z.string().datetime().openapi({ description: 'ISO 8601 timestamp' }),
}).openapi('User');

const document = generateDocument({
  openapi: '3.1.0',
  info: { title: 'Users API', version: '1.0.0' },
  paths: {
    '/users/{id}': {
      get: {
        requestParams: {
          path: z.object({ id: z.string().uuid() }),
        },
        responses: {
          200: {
            description: 'Fetch a user',
            content: { 'application/json': { schema: UserSchema } },
          },
        },
      },
    },
  },
});

The benefit is structural: because the same Zod schema validates incoming requests and shapes outgoing responses, the spec cannot disagree with runtime behavior.

Python with FastAPI

FastAPI remains the cleanest demonstration of the model. Decorators carry the full description:

from fastapi import FastAPI
from pydantic import BaseModel, Field

app = FastAPI(title="Orders API", version="2.0.0")

class Order(BaseModel):
    id: str = Field(..., description="Server-issued order ID")
    total_cents: int = Field(..., ge=0, description="Amount in cents, never floats")

@app.get("/orders/{id}", summary="Fetch an order", response_model=Order)
def get_order(id: str):
    ...

FastAPI exposes the generated spec at /openapi.json. The framework inspects type hints, Pydantic validators, Field metadata, and docstrings, then assembles a valid 3.1 document on every request. There is no separate build step.

Go with oapi-codegen

Go takes the inverse path: write the spec first, then generate server interfaces from it. This is "code from spec" rather than "spec from code," but it belongs in the same workflow. Teams that publish a stable contract to external consumers often prefer this model because partners can review the YAML before any implementation exists.

# openapi.yaml
oapi-codegen generates:
  types.go: Models
  server.go: HTTP interface

Schema-Derived Generation

When you want minimal intrusion, schema-derived tools read your type system and route registration directly.

Hono with hono-openapi

Hono is small enough that the spec generator can introspect the chain:

import { OpenAPIHono } from '@hono/zod-openapi';

const app = new OpenAPIHono();

app.openapi(
  createRoute({
    method: 'get',
    path: '/health',
    responses: {
      200: { description: 'Liveness probe', content: { 'application/json': { schema: HealthSchema } } },
    },
  }),
  (c) => c.json({ status: 'ok' })
);

app.doc('/openapi.json', { openapi: '3.1.0', info: { title: 'Health API', version: '1.0.0' } });

The route definition is the documentation. Adding a new endpoint is a single PR; the spec follows.

NestJS with @nestjs/swagger

NestJS combines both models. Class decorators (@ApiProperty) provide field-level metadata, while CLI plugins can derive response schemas from TypeScript classes without explicit decorators. The plugin approach is especially useful in legacy codebases where adding decorators to every DTO would be a multi-week project.

Runtime Introspection

Annotation and schema tools can miss anything that lives outside the route handler — middleware, request transforms, conditional headers. Runtime introspection tools like tsoa, express-openapi-validator, or proxy-based tools like Optic observe actual HTTP traffic and reconstruct the spec from observed shapes.

This is the only strategy that captures behavior you did not document. The downside is that undocumented behavior is often undocumented for a reason: it is accidental, and codifying it as a contract can lock in bugs. Use runtime tools to discover gaps, then promote the meaningful ones into explicit schemas.

Wiring Generation Into CI

Auto-generation only pays off if the output is checked in, reviewed, and published on every merge. A minimal pipeline:

# .github/workflows/docs.yml
name: API Docs
on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20 }
      - run: npm ci
      - run: npm run openapi:build  # writes ./dist/openapi.json
      - uses: actions/upload-artifact@v4
        with:
          name: openapi-spec
          path: dist/openapi.json
      - run: npx @redocly/cli lint dist/openapi.json

The lint step is the unsung hero. Spectral or Redocly CLI catches missing descriptions, inconsistent error schemas, and accidentally exposed internal endpoints before they reach a public docs portal.

Common Pitfalls

After several codebases of OpenAPI work, the same mistakes repeat:

  • Polling the endpoint for the spec at runtime in production. This couples every docs request to application latency. Build the spec at deploy time and serve it as a static file.
  • Allowing free-form descriptions. A spec without enforced field descriptions produces docs that look professional and say nothing. Treat descriptions as required fields in lint rules.
  • Mixing versions. Pin a single OpenAPI version per spec. 3.0 and 3.1 have different JSON Schema semantics, and tooling that supports both will produce subtle drift.
  • Forgetting auth metadata. The securitySchemes section is where you describe OAuth flows, API keys, and bearer tokens. Without it, consumers cannot call protected endpoints from the docs UI.

Keeping Docs Honest

Generation prevents drift between code and spec. It does not prevent drift between spec and reality. Three habits close the loop:

  1. Run contract tests against the published spec. Tools like schemathesis generate adversarial requests from the schema and compare responses to declared shapes.
  2. Review spec diffs in pull requests the same way you review code diffs. A 200-line schema change is not automatically correct.
  3. Version your spec alongside your code. Breaking changes deserve a new major version, an updated server URL, and a migration guide.

If you maintain a GitHub repository and would rather not wire all of this up by hand, DocPilot connects to your repos, detects your framework's OpenAPI output (or generates one from your route definitions), and keeps a README, setup guide, and full API reference in sync through webhooks. It handles the build, lint, and publish steps so your team can focus on shipping endpoints rather than describing them. The free tier covers public repositories and is worth a few minutes to evaluate against whatever pipeline you have today.

Ship documentation that keeps up

Try DocPilot on your repository — free for one repo.

Connect your first repository in about a minute. No credit card. DocPilot writes and maintains the documentation from your code.

Read next