A Practical Guide to Writing OpenAPI Specs That Don't Suck

Most OpenAPI specs are generated once, committed to a repo, and never updated again. Six months later, the spec describes an API that no longer exists. Consumers learn to ignore the docs and reverse-engineer the actual behavior from Postman collections shared in Slack threads.

It doesn't have to be this way. Here's how to write specs that stay useful.

Write the spec first, not last

The biggest mistake teams make is treating the OpenAPI spec as documentation. It's not documentation — it's a contract. Write it before you write the implementation, the same way you'd define a database schema before writing queries against it.

This doesn't mean you need to get it perfect upfront. Start with the paths, methods, and request/response shapes. Fill in descriptions and examples later. The point is to agree on the interface before building it.

Use examples everywhere

A schema definition without examples is like a function signature without a docstring — technically complete, formally useless. Every request body and response should include at least one example with realistic data.

openapi.yaml
# Bad: schema only
UserResponse:
  type: object
  properties:
    id:
      type: string
    email:
      type: string

# Good: schema with example
UserResponse:
  type: object
  properties:
    id:
      type: string
      example: "usr_a1b2c3d4"
    email:
      type: string
      format: email
      example: "dev@example.com"

Document error responses, not just happy paths

Every spec I've reviewed documents the 200 response beautifully and completely ignores everything else. Your consumers need to know what a 422 looks like, what error codes you return, and what the error body structure is.

At minimum, document 400 (validation errors), 401 (auth failures), 404 (not found), and 429 (rate limited). Use a consistent error schema across all endpoints.

Use $ref for shared schemas

Duplicating schema definitions across endpoints is how specs become unmaintainable. If your User object is returned by five different endpoints, define it once in components/schemas and reference it everywhere.

This also applies to error responses, pagination parameters, and common headers. If you copy-paste a schema definition, you've already lost.

Describe your authentication properly

The securitySchemes section exists for a reason. Don't just write "pass a Bearer token" in a description field somewhere. Define the scheme properly so that code generators and API clients can use it automatically.

security scheme
components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: API key from Settings > API Keys

security:
  - BearerAuth: []

Version your spec alongside your code

The spec file should live in the same repository as the API code it describes. It should be updated in the same pull request that changes the API behavior. If your spec is in a separate repo, wiki page, or Confluence doc, it will drift.

Add a CI check that validates the spec against the implementation. Tools like openapi-diff can catch breaking changes before they're merged. Treat spec changes with the same rigor you'd apply to database migrations.

Use operationId consistently

The operationId field is optional in the spec but critical for code generation and SDK quality. Name them consistently — listUsers, getUser, createUser, updateUser, deleteUser. Your consumers building typed clients will thank you.

Don't auto-generate and forget

Auto-generated specs from frameworks like FastAPI, NestJS, or Spring are a reasonable starting point. But they often produce specs that are technically valid and practically useless — missing descriptions, generic names, no examples, and overly complex type definitions.

Use the auto-generated spec as a baseline, then curate it. Add descriptions. Add examples. Simplify unnecessary oneOf/anyOf constructs. The goal is a spec that a developer can read and understand without also reading your source code.

Keep it simple

OpenAPI supports an enormous amount of complexity — polymorphism, callbacks, links, discriminators. Most APIs don't need any of it. Use the simplest constructs that accurately describe your API. A spec that's easy to read is more valuable than one that's technically complete but impossible to understand.

← All posts