How to generate an OpenAPI spec from existing code
Four working ways to get an OpenAPI 3.1 spec out of a codebase that never had one, what each approach misses, and how to keep the result true.
- Four working routes to a spec from existing code: annotations, schema-first frameworks, traffic recording, and static analysis.
- Static analysis covers existing codebases with no code changes; schema-first frameworks make the spec free on new services.
- Whichever generates it, wire regeneration plus a breaking-change diff into CI, or the spec starts lying immediately.
Most APIs in production have no OpenAPI spec, and most that do have one that lies. If you need a spec today, for a gateway, a client SDK, a security review, or an MCP server, you have four realistic ways to produce one from the code you already have. They differ in coverage, accuracy, and how much they cost to keep true, which is the part everyone underestimates. Writing the file once is a weekend. Keeping it honest is the actual problem.
Option 1: annotations in the code
Every major ecosystem has a library that builds a spec from decorators or comments: swagger-jsdoc for Express, springdoc for Spring, drf-spectacular for Django REST Framework, attributes in ASP.NET Core. You annotate each handler, and the spec is assembled at build or runtime.
This works well when the team adopts it from day one and reviews annotations like code. On an existing codebase it means touching every handler, and the annotations become a second description of behavior that can drift from the first. A comment saying a field is required does not make it required. Coverage in practice tracks team discipline, and discipline is exactly what a documentation backlog indicates you are short on.
Option 2: schema-first frameworks
If your framework derives both validation and documentation from one schema definition, the spec is close to free and close to true. FastAPI generates OpenAPI from Python type hints and Pydantic models. NestJS does it from TypeScript decorators plus DTO classes. Fastify builds it from the JSON Schema you attach to each route. tRPC and similar RPC layers can emit OpenAPI through plugins.
The catch is the qualifier: your framework. This option is a strong argument for choosing such a framework on new services, and no help at all for the nine-year-old Express service where the highest-traffic endpoints live. Migrating a working API onto a schema-first framework to get documentation is surgery nobody schedules.
Option 3: record real traffic
Traffic-based tools watch requests in a gateway, proxy, or middleware and infer a spec from what actually flows: paths, methods, observed fields, and types. This captures ground truth, including endpoints the team forgot exist, and it requires no code changes.
Its blind spots are structural. It cannot see endpoints that received no traffic during the observation window, which disproportionately hides admin routes, error paths, and rarely used partner features, precisely the endpoints audits care about. It infers optionality from absence, so a field that happened to always be present gets marked required. And it needs production access plus a privacy review, because the recorder sees real payloads.
Option 4: static analysis of the repo
The fourth approach reads the code the way a compiler does: find the router registrations, resolve the handlers, walk the validators and serializers, and emit the spec from what the code will actually accept and return. Route tables are machine-readable by necessity, and validation libraries (zod, Joi, Pydantic, Bean Validation) encode the schema you would otherwise transcribe by hand.
Done well, this yields complete endpoint coverage without running the service or touching production, including codebases that never had a spec. The honest limits: dynamically constructed routes need runtime hints, and no analyzer can extract intent. What a field means, which values are safe to show a partner, why an endpoint exists: that layer is human, and the right workflow layers it on top of the generated baseline instead of asking people to also transcribe the mechanical parts. This is the approach Elva takes when it reads a repo, regenerating the spec on every commit so the baseline cannot drift.
Choosing, in practice
| Approach | Coverage | Code changes needed | Keeps itself true? |
|---|---|---|---|
| Annotations | Tracks team discipline | Every handler | No |
| Schema-first framework | Full, new services | Framework choice | Yes |
| Traffic recording | Only trafficked routes | None | Partially |
| Static analysis | Full, existing code | None | Yes, on every commit |
- New service, greenfield: pick a schema-first framework and get the spec for free forever.
- Existing service, small surface, motivated team: annotations, enforced in review.
- Unknown surface, need ground truth about usage: traffic recording, as a complement rather than the source of record.
- Existing services at any scale, spec needed without code changes: static analysis, with human descriptions layered on top.
Teams usually end up combining two: a generated structural baseline plus human intent annotations, with traffic data validating both.
Whatever generates it, wire it to CI
A spec produced once is a snapshot that starts aging immediately. The property that makes any of these approaches durable is regeneration plus a diff gate: the spec rebuilds on every commit, the build compares it to the previous version, and a change that removes a field or narrows a type is flagged as breaking before it merges. At that point the spec stops being documentation and starts being an enforced contract, which is the version of this work that pays for itself. Target OpenAPI 3.1 while you are at it: it aligned with standard JSON Schema, and every serious toolchain now consumes it.
FAQ
Can I generate an OpenAPI spec without writing annotations?
Yes. Static analysis reads route registrations, validators, and serializers and emits the spec from what the code actually accepts and returns. Traffic recording is an alternative, with blind spots for endpoints that saw no traffic.
Which spec generation method should I choose?
Schema-first frameworks for new services, annotations for small motivated teams, static analysis for existing codebases at any scale. Most teams combine a generated structural baseline with human intent descriptions on top.
How do I keep a generated spec accurate?
Regenerate it on every commit and gate merges on the diff. A change that removes a field or narrows a type should be flagged as breaking before it lands, which turns the spec from documentation into an enforced contract.
Ship notes, monthly
One email with what shipped and what we learned. Unsubscribe anytime.