API strategy

Platform integration

How to generate an OpenAPI spec from an existing codebase

How to generate an OpenAPI spec from an existing codebase: framework introspection, annotations, validation with Spectral, and CI drift detection in seven steps.

7 minute read
Decorative imagery showcasing Pontil's brand

By the end of this guide you'll have a working OpenAPI 3.1 specification generated directly from an existing codebase — not hand-written, not aspirational, and not out of date the moment a route changes. We'll cover the three viable strategies (framework introspection, decorator-driven generation, and traffic capture), then walk through a concrete framework-introspection setup with FastAPI, Express, and Spring Boot as worked examples.

Prerequisites: a running application with HTTP routes, admin access to its source, and around 60–90 minutes for the initial spec. You'll need Node 20+, Python 3.11+, or a JDK 17+ depending on your stack.

Step 1 — Pick your generation strategy

There are three ways to reverse engineer OpenAPI from code, and they don't work equally well. Choose based on how your routes are declared, not on what looks easiest.

Framework introspection. The framework already knows its own routes, handlers, and types. A generator reads that in-memory model at boot and emits OpenAPI. This is the most accurate approach when the framework supports it — FastAPI, NestJS, Spring Boot with springdoc-openapi, ASP.NET Core with Swashbuckle. Use this when you have it.

Decorator or annotation-driven. You add annotations to handlers (@ApiOperation, JSDoc @openapi blocks, Python decorators) and a build-time tool assembles a spec. Works for Express, Flask, Koa, Fastify without built-in schema support. More manual, but gives you control over shapes the framework can't infer.

Traffic capture and inference. A proxy watches live requests and responses, then infers schemas. Tools like Optic, Speakeasy's traffic capture, and APIToolkit do this. (Akita, once the go-to option here, was acquired by Postman and folded into their platform.) Use as a last resort — captured specs reflect what your API did, not what it should do, and inferred schemas need heavy cleanup.

A quick decision table:

Framework introspection
Decorator-driven
Traffic capture

Accuracy

High

High if disciplined

Approximate

Setup time

Minutes

Hours

Hours + tuning

Handles undocumented routes

Yes

No

Yes

Reflects intent vs behaviour

Intent

Intent

Behaviour

Best for

Typed frameworks

Untyped web frameworks

Legacy or opaque services


If you're not sure your framework supports introspection, check the framework's docs before writing any code — the generator almost always already exists.

Step 2 — Install the generator for your framework

Install the generator that matches your stack. Below are the three most common cases.

FastAPI (Python). OpenAPI generation is built in. No extra install needed. Confirm you have a recent version:

pip show fastapi | grep -i version

Express (Node). For introspecting an existing app, use @wesleytodd/openapi or express-oas-generator. For annotation-driven generation from JSDoc, use swagger-jsdoc plus swagger-ui-express:

npm install swagger-jsdoc swagger-ui-express
npm install -D @types/swagger-jsdoc @types/swagger-ui-express

Spring Boot (Java). Add springdoc-openapi to your pom.xml:

<dependency>
 <groupId>org.springdoc</groupId>
 <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
 <version>2.8.17</version>
</dependency>

Run the app after installing. If the generator can find your routes at all, you'll see a spec at /openapi.json, /v3/api-docs, or an equivalent path.

Step 3 — Annotate what the framework cannot infer

The generator will pick up route paths, HTTP methods, and typed parameters automatically. It won't pick up:

  • Response shapes when handlers return untyped dictionaries or any.
  • Error responses beyond the default 200.
  • Auth requirements at the operation level.
  • Semantic meaning behind opaque IDs (which are user IDs vs order IDs).

Add the missing detail where the framework expects it.

FastAPI — use Pydantic models as response types and add responses= for error codes:

from pydantic import BaseModel
from fastapi import HTTPException

class Order(BaseModel):
   id: str
   total_cents: int
   status: str

@app.get(
   "/orders/{order_id}",
   response_model=Order,
   responses={404: {"description": "Order not found"}},
)
def get_order(order_id: str) -> Order:
   ...

Express — add JSDoc blocks above each handler:

/**
* @openapi
* /orders/{orderId}:
*   get:
*     summary: Get an order by ID
*     parameters:
*       - in: path
*         name: orderId
*         required: true
*         schema: { type: string }
*     responses:
*       200:
*         description: Order found
*         content:
*           application/json:
*             schema: { $ref: '#/components/schemas/Order' }
*       404: { description: Not found }
*/
app.get('/orders/:orderId', getOrder);

Spring Boot — use @Operation and @ApiResponses from springdoc:

@Operation(summary = "Get an order by ID")
@ApiResponses({
   @ApiResponse(responseCode = "200", description = "Order found"),
   @ApiResponse(responseCode = "404", description = "Not found")
})
@GetMapping("/orders/{orderId}")
public Order getOrder(@PathVariable String orderId) { ... }

Do one representative handler per resource first. Get the shape right, then propagate.

Step 4 — Export the spec to a file

A spec served at a runtime endpoint is fine for humans reading it in a browser. For CI, SDK generation, and agent tooling you need it on disk, versioned in the repo.

FastAPI:

import json
from app.main import app

with open("openapi.json", "w") as f:
   json.dump(app.openapi(), f, indent=2)

Express with swagger-jsdoc:

import fs from 'node:fs';
import swaggerJSDoc from 'swagger-jsdoc';

const spec = swaggerJSDoc({
 definition: { openapi: '3.1.0', info: { title: 'API', version: '1.0.0' } },
 apis: ['./src/routes/*.ts'],
});
fs.writeFileSync('openapi.json', JSON.stringify(spec, null, 2));

Spring Boot — hit the runtime endpoint from a build step:

curl http://localhost:8080/v3/api-docs > openapi.json

Commit openapi.json (or openapi.yaml) to the repo. Now diffs show up in code review.

Step 5 — Validate the spec

Generated specs are often syntactically valid but semantically wrong: missing operation IDs, duplicate paths, response schemas that don't match the model. Run a validator before you trust the output.

Install Spectral — the de facto OpenAPI linter:

npm install -g @stoplight/spectral-cli
spectral lint openapi.json

Expect warnings on the first pass. The ones worth fixing immediately:

  • operation-operationId — every operation needs a stable, unique ID. SDK generators use it as the method name.
  • operation-description — a one-line description per operation. Agent tool descriptions come from this field.
  • Missing 4xx responses on write operations.
  • $ref targets that don't resolve.

For writing specs that both humans and machines can actually use, see our guide on OpenAPI specification best practices.

Step 6 — Wire it into CI

Regenerate the spec on every build and fail the pipeline when it drifts. Otherwise the spec goes stale within a sprint and every consumer downstream — SDKs, docs, agent tools — inherits the drift.

A GitHub Actions job that catches drift:

- name: Regenerate OpenAPI spec
 run: python scripts/dump_openapi.py

- name: Fail on drift
 run: |
   git diff --exit-code openapi.json || (
     echo "openapi.json is out of date. Regenerate and commit." && exit 1
   )

- name: Lint spec
 run: npx @stoplight/spectral-cli lint openapi.json

- name: Detect breaking changes
 run: npx oasdiff breaking origin/main:openapi.json openapi.json

The oasdiff step catches breaking changes before merge. For the full pattern, see how to detect API breaking changes.

Step 7 — Fill the gaps the generator missed

Run through the spec by hand once. The generator gives you a skeleton; a spec that survives contact with SDK generators and agent tooling needs more.

Check each operation for:

  • Examples. Add example values on request bodies and responses. Agent tools use examples to disambiguate parameters when descriptions are thin.
  • Enums. Any field with a fixed set of values should be an enum, not type: string.
  • Nullability. type: ["string", "null"] in OpenAPI 3.1, or nullable: true in 3.0. Missing nullability is the single most common source of downstream SDK bugs.
  • Security schemes. Declare them once in components.securitySchemes and reference per operation. Don't skip this — auth is not obvious from route code.
  • Pagination shape. Cursor, offset, or page. Document the response envelope, not just the item schema.

This is the phase where a generated spec becomes a spec worth publishing. Skipping it is why so many autogenerated specs are technically valid and practically useless.

Common pitfalls

The spec regenerates cleanly but SDKs break. Almost always because operation IDs are unstable — the generator names them from function names, and a rename cascades. Pin operation IDs explicitly on every operation.

Response schemas are object with no properties. Handler returns an untyped dict. Fix at the source: introduce a typed model. Don't paper over it in the spec.

Internal routes leak into the public spec. Health checks, admin endpoints, debug routes. Tag them or exclude them via the generator's include/exclude config. Public consumers should never see them.

Two routes, one path. The generator emits one and silently drops the other. Check the route count in your framework against the operation count in the spec. If they don't match, something got merged.

Traffic-captured specs never converge. If you went the capture route in Step 1 and your spec keeps changing on every replay, your API is inconsistent — the tool is telling you the truth. Fix the API before trusting the spec.

The spec is generated but nothing consumes it. Feed it into your SDK generator, docs site, and — if you're building agent tooling — your tool definitions. A spec no one reads decays regardless of how it was produced.

Join our weekly newsletter

Stay up to date on the ever changing agentic landscape.

POSTS

Related content

API strategy

Platform integration

OpenAPI specification best practices: writing specs agents and SDK generators can actually use

9 minute read

API strategy

Platform integration

How to detect API breaking changes: a practical guide for agent-era teams

7 minute read

API strategy

Platform integration

API discoverability for AI agents: why your docs aren't the interface anymore

5 minute read