OpenAPI Specs: The Documentation Format Worth Getting Right From the Start
by Eric Hanson, Backend Developer at Clean Systems Consulting
What you are actually creating
An OpenAPI specification is not documentation in the traditional sense — it is a machine-readable contract. Everything downstream flows from it: generated client SDKs, contract tests, API gateway configurations, interactive documentation portals, mock servers for frontend development.
When the spec is accurate, all of these stay in sync automatically. When the spec drifts from the implementation, none of them can be trusted. The quality of your OpenAPI spec is a force multiplier on everything that consumes it.
Code-first vs. spec-first
Code-first: You write the implementation, then generate or annotate the spec from the code. The spec is derived from reality.
Tools: springdoc-openapi for Spring Boot, FastAPI's built-in generation, drf-spectacular for Django REST Framework, tsoa for TypeScript/Express.
Pros: spec is always synchronized with the running code. No risk of implementation divergence.
Cons: the spec reflects what you built, not what you should have built. Design feedback comes after implementation, when changes are more expensive.
Spec-first: You write the OpenAPI spec before implementation. Code is generated or scaffolded from the spec.
Tools: OpenAPI Generator for scaffolding, Stoplight Studio for GUI-based spec editing, Redocly for spec linting and organization.
Pros: the spec is a design artifact — you can review it, test it with mock servers, and share it with consumers before writing a line of implementation code. Design problems surface cheaply.
Cons: requires discipline to keep spec and implementation in sync if you are not generating implementation from the spec. Spec generators for every language do not produce production-ready code — they produce scaffolding you then maintain manually.
The hybrid approach that works in practice: Design in the spec (spec-first for the design phase), generate scaffolding, implement against the scaffolding, then use code-generation tools to maintain the spec as you iterate. Run spec validation in CI to catch divergence.
What a well-written schema looks like
The difference between a schema that helps developers and one that merely compiles:
Minimal (compiles but not useful):
components:
schemas:
Order:
type: object
properties:
id:
type: string
status:
type: string
amount:
type: number
Complete (actually documents the contract):
components:
schemas:
Order:
type: object
required: [id, status, amount, currency]
properties:
id:
type: string
format: ulid
example: "01HZQK7P3WVXBN4Y9MRDTJC8E6"
description: Unique order identifier (ULID format)
readOnly: true
status:
type: string
enum: [pending, confirmed, shipped, delivered, cancelled]
description: |
Order lifecycle state.
Clients should handle unknown values gracefully — new states
may be added in minor API versions.
example: confirmed
amount:
type: integer
description: |
Order total in minor currency units (e.g., cents for USD).
Always an integer. Never use floating point for monetary values.
example: 4999
minimum: 1
currency:
type: string
pattern: '^[A-Z]{3}$'
description: ISO 4217 currency code
example: USD
The required array is often omitted. Without it, code generators cannot determine which fields to mark as mandatory in generated types. Every field should be explicitly required or optional.
format hints for common patterns — ulid, date-time, email, uri — help code generators produce correct types. readOnly: true prevents code generators from including id in request body types.
The note about unknown enum values handling is a versioning policy embedded in the documentation — the right place for it.
Keeping the spec accurate over time
The single biggest failure mode for OpenAPI specs: they are accurate at launch and drift from reality over the following months as the implementation changes and the spec does not.
Prevention:
Validate the spec against running responses in CI. Tools like Schemathesis run property-based tests against your API using the OpenAPI spec as the test oracle — it generates requests based on the spec and validates that responses match the declared schemas. This catches divergence automatically:
schemathesis run openapi.yaml --url http://localhost:8000 --checks all
Require spec updates in the same PR as implementation changes. Make the spec a first-class part of the codebase. A PR that changes a response shape without updating the spec should not pass review.
Generate the spec from annotations where possible. For frameworks that support it, maintaining the spec as code comments or annotations tied to the implementation is more reliable than maintaining a separate YAML file:
@Operation(summary = "Retrieve an order")
@ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = OrderResponse.class)))
@ApiResponse(responseCode = "404", content = @Content(schema = @Schema(implementation = ApiError.class)))
@GetMapping("/orders/{id}")
public ResponseEntity<OrderResponse> getOrder(@PathVariable String id) { ... }
The spec as a design review tool
Before implementation, circulate the spec. Ask:
- Do the endpoint names and paths match how engineers and product think about the domain?
- Are the request and response shapes what a client would find natural to work with?
- Are error responses documented for all failure modes, not just happy paths?
- Are all required fields actually required? Are all optional fields actually optional?
A PR-based review of an OpenAPI spec change is cheaper than a code review of a poorly designed implementation. Getting feedback at spec time costs minutes. Getting the same feedback after clients have integrated costs version bumps.