Most API documentation is wrong. Not deliberately — it was accurate when written, then an endpoint gained a field, a status code changed, a parameter became required, and nobody updated the page. The reader discovers the discrepancy at the worst possible moment, and eventually stops trusting the documentation entirely, which means the API's real contract lives in someone's head.
OpenAPI exists to solve this by making the contract a machine-readable artefact that documentation, validation, tests, clients and mocks are all generated from. When that artefact is the source of truth rather than a description produced afterwards, drift becomes impossible rather than merely discouraged.
What you will learn
- What OpenAPI and Swagger actually are, and how they relate
- The structure of a specification, explained in plain terms
- Design-first versus code-first, and which suits your situation
- Everything you can generate: docs, clients, servers, mocks, validation, tests
- How to enforce the contract in a pipeline so it cannot drift
- Versioning, governance and the mistakes that make specifications useless
- OpenAPI and Swagger, disambiguated
- Why a machine-readable contract matters
- The anatomy of a specification
- Schemas and reuse
- Design-first or code-first
- Documentation
- Generating clients and servers
- Request and response validation
- Mock servers
- Contract testing
- Enforcing it in the pipeline
- Versioning and breaking changes
- Governance and style
- Limits of the format
- Twelve mistakes
- A worked example: one endpoint, fully wired
- Frequently asked questions
1. OpenAPI and Swagger, disambiguated
The naming causes persistent confusion, and the history explains it.
Swagger was originally the name of both a specification format and a set of tools built around it. When the specification was donated to an open governance body, it was renamed OpenAPI, while the tooling kept the Swagger name.
So today: OpenAPI is the specification — a standard format for describing an HTTP API. Swagger is a family of tools that work with it, most visibly an interactive documentation renderer and an editor. Other tool families exist and are frequently better; the specification is what matters, and it is vendor-neutral.
Version-wise, most current work uses the third major version of the specification, with a fourth introducing a more modular structure. Older second-version documents are still widespread and convert reasonably well, though some newer capabilities have no equivalent.
2. Why a machine-readable contract matters
The value is not documentation. It is that one artefact can drive many things that would otherwise be maintained separately and inconsistently.
| From the specification | You get | What it replaces |
|---|---|---|
| Rendering | Interactive documentation | Hand-written pages that drift |
| Code generation | Typed clients in many languages | Hand-written HTTP wrappers per consumer |
| Server scaffolding | Route and model stubs | Boilerplate written by hand |
| Runtime validation | Requests and responses checked against the contract | Ad-hoc validation that diverges from docs |
| Mocking | A working fake service | Consumers blocked until the API exists |
| Contract testing | Automated verification of conformance | Discovering breakage during integration |
| Linting | Consistency rules enforced automatically | Style debates in code review |
The compounding effect is the point. Each capability individually is a convenience; together they mean that the contract cannot be violated silently, because something in the pipeline notices.
3. The anatomy of a specification
A specification document has a small number of top-level sections, and understanding what each is for makes the format readable without memorising it.
Info describes the API itself: title, version, description, contact and licence. The version here is the API's version, not the specification format's.
Servers lists the base URLs the API is available at — typically production, staging and a local address. This is what lets generated clients and interactive documentation actually send requests.
Paths is the bulk of the document. Each path contains the operations available on it, and each operation describes its parameters, request body, possible responses, security requirements and a summary.
Components holds reusable pieces: schemas, parameters, responses, security schemes and examples. This is where a well-structured specification does most of its work, because repetition is what makes specifications unmaintainable.
Security declares the authentication schemes available and which apply by default, with operations able to override.
Tags group operations for presentation, usually by resource. Documentation renderers use these for navigation, so tagging carelessly produces documentation nobody can navigate.
4. Schemas and reuse
Schemas describe the shape of data, and they are where specifications succeed or become unmaintainable.
The discipline is straightforward: define each concept once in components and reference it everywhere. A customer schema defined once and referenced by six operations means a field added to a customer appears in all six. The same schema copied into six operations means five of them are wrong within a quarter.
Several patterns are worth knowing because they recur constantly:
- Composition lets a schema extend another — an error response with common fields plus operation-specific detail — rather than duplicating the common part.
- Discriminated unions describe responses that can be one of several shapes, with a field indicating which. Generated clients handle these well when the discriminator is declared and badly when it is not.
- Separate request and response schemas for the same concept. A create request has no identifier and no timestamps; the response has both. Forcing one schema to serve both means every field becomes optional, which discards most of the contract's value.
- Explicit nullability and requiredness. A field that is present but null is different from a field that is absent, and clients handle them differently. Leaving this ambiguous produces integration bugs that are tedious to diagnose.
Two habits improve every specification. Add examples to schemas and operations, because readers copy examples and generated documentation is far more useful with them. And write descriptions that explain semantics rather than restating the field name — a description reading "the customer identifier" for a field named customer identifier adds nothing, while one explaining which system issues it and whether it is stable adds a great deal.
5. Design-first or code-first
Two workflows, each with a legitimate case.
Design-first means writing the specification before the implementation. The specification is reviewed, consumers can begin against a mock immediately, and the server is built to satisfy an agreed contract. This suits public APIs, APIs consumed by other teams, and anything where the interface is a negotiated commitment rather than an implementation detail.
Code-first means generating the specification from annotations or types in the implementation. It stays automatically in sync, requires no separate artefact to maintain, and suits internal APIs consumed only by their own front end, where the interface follows the implementation naturally.
| Design-first | Code-first | |
|---|---|---|
| Contract agreed | Before implementation | After, implicitly |
| Consumers unblocked | Immediately, via mock | When the API is built |
| Drift risk | Implementation may diverge — needs validation | Low, but the design is never reviewed |
| Review of the interface | Explicit, before cost is sunk | Rarely happens |
| Best for | Public and cross-team APIs | Internal APIs with one consumer |
The hybrid that works well in practice: design-first for the interface, code-first for keeping it honest. Write the specification, review it, generate types from it so the implementation must satisfy it, and validate responses against it in tests. That gets the review benefit without the drift risk.
6. Documentation
Rendering a specification produces browsable, interactive documentation where each operation can be executed against a real server. This is genuinely useful and it is not sufficient on its own.
Generated reference documentation answers "what does this endpoint do". It does not answer "how do I accomplish this task", which is what a new integrator actually needs. A complete documentation set has three parts: a quick start that produces a successful call within five minutes, task-oriented guides for the common journeys, and the generated reference for detail.
Things that make generated documentation substantially better, in rough order of value: realistic examples on every operation including error responses; descriptions that explain semantics and constraints rather than restating names; documented error codes with what the caller should do about each; and clear authentication instructions at the top, because that is where most integrators get stuck first.
7. Generating clients and servers
Client generation produces a typed library in your consumer's language from the specification. The benefit is real: consumers get autocompletion, compile-time checking against the contract, and updates by regenerating rather than by reading a changelog.
The caveats are worth knowing. Generated clients vary enormously in quality by language and generator. They can be verbose and idiosyncratic. And regenerating on every specification change means consumers experience churn — pinning a generated client to a specification version and upgrading deliberately is usually better than tracking the latest.
Server generation produces routing and model stubs. This is most valuable at the start of a project and less so afterwards, since regenerating over an implemented server requires care to avoid overwriting work. The more durable pattern is generating types rather than whole servers: the implementation must satisfy types derived from the specification, so a mismatch is a compile error rather than a runtime surprise.
8. Request and response validation
Validating at runtime against the specification is where the contract stops being aspirational.
Request validation rejects malformed input before it reaches your handlers, using rules taken directly from the specification rather than duplicated in code. This eliminates the common divergence where documented validation and implemented validation differ.
Response validation is the more valuable and less commonly used direction. Checking that your responses conform to what you promised catches the exact class of drift that makes documentation untrustworthy — a field quietly renamed, a type changed, a required property omitted under some condition.
The practical arrangement is to enable response validation in development and test environments, where a violation should fail loudly, and to log rather than reject in production, where breaking a response to enforce a contract would harm users more than the violation does.
9. Mock servers
A mock returns responses conforming to the specification without any implementation behind it. Two uses justify it.
Unblocking consumers. Agree the contract, generate a mock, and the client team builds while the server team implements. Both meet at the specification rather than negotiating late, which is the single largest scheduling benefit of design-first work.
Testing error handling. Reproducing a 503 or a rate-limit response from a real service is awkward; a mock returns one on request. This is how you verify that a client genuinely handles failures rather than assuming it does.
Mocks can return either the examples you supplied or values generated from the schema. Supplied examples are more realistic and worth the effort; generated values are useful for checking that a client handles the full range of a schema rather than only the happy example.
The risk is a mock diverging from the real implementation, at which point consumers build against a fiction. Running the same contract tests against both is what prevents it.
10. Contract testing
Contract testing verifies that an implementation actually conforms to its specification, and it is the practice that converts a specification from documentation into an enforceable agreement.
Two approaches, frequently used together:
Specification-driven testing generates requests from the specification and checks that responses conform. It covers breadth automatically — every operation, every documented status code — and requires little maintenance because it derives from the specification you already keep.
Consumer-driven contract testing records what each consumer actually depends on, and verifies the provider satisfies those expectations. Its advantage is precision: you learn not just that a change is technically breaking, but whether anyone is affected. This makes deprecation decisions evidential rather than nervous.
The practical guidance is to start with specification-driven verification, which is cheap and broad, and add consumer-driven contracts at the boundaries where several teams integrate and coordination costs are high.
11. Enforcing it in the pipeline
A specification that is not checked automatically will drift, because it depends on individual diligence. Four checks, cheap to add:
- Lint the specification. Style and completeness rules — every operation has a description, every response has an example, naming is consistent, no undocumented status codes. This ends most style debates by making them automatic.
- Detect breaking changes. Compare the specification against the previous version and fail the build on a breaking change unless it is deliberately acknowledged. This is the single highest-value check available, because it converts an accidental break into a conversation.
- Verify conformance. Run the implementation against the specification, checking that responses match. Failures here are drift, caught before release.
- Publish on merge. Documentation, generated clients and the mock all updated from the merged specification, so consumers always see current reality.
With those four in place, the specification is load-bearing: it cannot silently disagree with the implementation, because the pipeline notices.
12. Versioning and breaking changes
The best versioning strategy is needing it rarely, which follows from one rule: add, never remove or repurpose.
| Change | Breaking? |
|---|---|
| Adding an optional response field | No — clients must ignore unknown fields |
| Adding an optional request parameter | No |
| Adding a new operation | No |
| Removing or renaming a field | Yes |
| Changing a type or format | Yes |
| Making an optional parameter required | Yes |
| Adding a value to an enumeration | Usually yes in practice |
| Tightening validation | Yes — previously valid requests now fail |
| Adding a new required response field | No for clients, yes for strict validators |
The enumeration row surprises people. Consumers write exhaustive branches, so a new value causes failures in the field. Document from the outset that clients must handle unknown enumeration values, and treat adding one as a deliberate change rather than an addition.
When a version bump is genuinely required, keep one specification per API version, publish both during the transition, mark the old one deprecated in its description, and use consumer-driven contracts or access logs to know who is still calling before removing anything.
13. Governance and style
Once several teams write specifications, consistency becomes the problem. Two APIs in one organisation using different conventions for pagination, errors and naming force every integrator to learn twice.
The mechanism that works is a shared style ruleset enforced by a linter, covering the decisions that would otherwise be argued individually: naming conventions for paths and properties, a standard error shape, standard pagination parameters, required descriptions and examples, and which status codes are acceptable for which situations.
Publish shared components — the error schema, pagination parameters, common security schemes — in a central location that specifications reference rather than redefine. This is the same reuse discipline that applies within a specification, applied across an organisation.
Keep the ruleset short enough that people accept it and strict enough that it means something. A hundred rules produce warnings nobody reads; fifteen well-chosen ones produce consistency.
14. Limits of the format
Being clear about what a specification cannot express prevents disappointment.
- Cross-field validation. "This field is required only when that one has a particular value" is awkward to express and poorly supported by tooling. Document it in prose and validate it in code.
- Business rules. A specification describes shapes, not semantics. That a discount cannot exceed the order total is not expressible.
- Sequencing. That one operation must precede another belongs in guides, not in the specification.
- Rate limits and quotas. Describable in prose and headers, not as enforceable structure.
- Asynchronous and event-driven APIs. A different specification family exists for message-based interfaces; OpenAPI describes request-response.
- Behaviour under failure. Whether an operation is safe to retry, and what partial failure means, must be documented explicitly.
The practical response is to use the specification for everything structural and to maintain concise prose guides for the semantics — with both published together, so an integrator does not have to know which document holds which kind of truth.
15. Twelve mistakes
- Writing the specification after the API. It becomes a description rather than a contract, and nobody reviews the design.
- Duplicating schemas instead of referencing them. Five copies diverge within a quarter.
- One schema for request and response. Every field becomes optional and the contract loses meaning.
- No examples. Documentation that is technically complete and practically unusable.
- Descriptions that restate field names. Effort spent producing no information.
- Undocumented error responses. Consumers discover them in production.
- No breaking-change detection. The most valuable pipeline check, and the most often missing.
- No conformance verification. The specification and the implementation drift silently.
- Generated reference treated as complete documentation. No quick start, no task guides.
- Mocks that diverge from the implementation. Consumers building against a fiction.
- Inconsistent conventions across an organisation. Every integrator learns the same concepts repeatedly.
- Adding enumeration values casually. Exhaustive client branches fail in the field.
16. A worked example: one endpoint, fully wired
Consider adding a single endpoint — listing a customer's orders with filtering and pagination — to an existing API, done design-first with everything wired.
The specification is written first, and reviewed as a pull request before any implementation exists. The review catches three things that would otherwise have shipped: the filter parameter was named inconsistently with an equivalent filter elsewhere in the API, the pagination style differed from the organisation's standard, and nobody had decided what happens when the customer exists but has no orders. All three are cheaper to resolve in a specification review than after clients depend on the answer.
Schemas are referenced, not written inline. The order summary schema already exists in components and is reused. The pagination envelope comes from the organisation's shared components file. The error response is the standard shape every API in the organisation returns. Roughly two-thirds of the endpoint's specification is references rather than new definitions, which is what a healthy specification looks like.
The linter runs on the pull request and fails initially: the new operation has no example for its error response, and one description restates the field name. Both are fixed in a minute. Nobody argued about either, because the rule was already agreed.
The mock is published on merge, and the client team begins building against it that afternoon. They also test their error handling by requesting the mock's failure responses, which is the first time anyone has verified that path — and it turns out the client displayed a blank list rather than an error message for a 500.
Types are generated from the specification into the server project, so the handler must return a shape satisfying the contract. A field the implementer would otherwise have named slightly differently is caught at compile time rather than in integration.
Conformance verification runs in the pipeline, sending requests derived from the specification against the deployed service and checking responses. It catches a real discrepancy: under an empty result the implementation returned a null rather than an empty array, which conformed to nobody's expectation and would have broken at least one consumer.
Breaking-change detection compares against the previous specification and passes, because everything added is additive. Two months later, when someone proposes renaming a field for clarity, the same check fails the build and turns a silent break into a five-minute conversation about whether the clarity is worth a deprecation cycle.
The total additional effort over writing the endpoint conventionally is perhaps half a day, most of it one-off pipeline setup. What it buys is an interface that was reviewed before it was expensive to change, consumers unblocked days earlier, error handling actually tested, and a contract that cannot drift without something failing.
17. Frequently asked questions
Design-first or code-first for a new project?
Design-first if anyone outside your team will consume the API, because reviewing an interface before it is expensive to change is where most of the value sits. Code-first is defensible for an internal API with a single consumer that ships alongside it. The hybrid — design-first for the interface, generated types to keep the implementation honest — gets most of both.
Should we generate clients for consumers?
Offer them, and do not require them. Generated clients are valuable for consumers who want type safety and update by regeneration; they are verbose and idiosyncratic in some languages, and experienced integrators often prefer to write their own thin wrapper. Publishing the specification is the essential part; generated clients are a convenience layered on top.
How do we handle authentication in the specification?
Declare the security schemes in components and apply them at the document level, with operations overriding where they differ — a public health check, for example. Document how to obtain a token in prose, since that flow is not expressible in the specification, and make it the first thing an integrator reads. Authentication is where most people get stuck first.
Can a specification be too large?
Yes. A single document covering two hundred operations becomes slow to render, difficult to review and awkward to navigate. Split by resource area using file references, keep shared components in their own file, and assemble for publication. If the API is genuinely that large, consider whether it should be several APIs with separate specifications and separate ownership.
What about GraphQL or event-driven APIs?
OpenAPI describes request-response HTTP APIs. GraphQL has its own schema language that serves an equivalent role. Event-driven and message-based interfaces have a separate specification family with the same philosophy — a machine-readable contract driving documentation, validation and code generation. The underlying discipline transfers even though the format does not.
How do we get an existing undocumented API specified?
Incrementally, starting with the endpoints most consumed. Some tooling can generate a starting specification from traffic or from existing annotations, which is a useful first draft and rarely correct enough to publish unedited. Add conformance verification as soon as you have a few endpoints specified, so the specification stops drifting further while you work through the rest.
Who should own the specification?
The team that owns the API, with review from consumers for anything public or cross-team. What fails is ownership by a documentation function separate from engineering, because the specification then lags the implementation permanently. Treating it as code — in the repository, reviewed in pull requests, checked by the pipeline — is what keeps ownership where it belongs.
What is the single highest-value thing to add first?
Breaking-change detection in the pipeline. It requires a specification to exist and then prevents the failure mode that damages consumers most — an interface change nobody intended to make. Conformance verification is a close second, because it stops the specification and the implementation from drifting apart while everyone assumes they agree.
Glossary
| Term | What it means |
|---|---|
| Specification | The machine-readable document describing the API. The contract, not a description of it. |
| Operation | One method on one path — a GET on the orders collection is one operation, a POST is another. |
| Component | A reusable definition — schema, parameter, response, security scheme — referenced from many places. |
| Reference | A pointer to a component. The mechanism that prevents duplication and therefore drift. |
| Schema | The shape of a piece of data: its properties, their types, which are required, and constraints on values. |
| Discriminator | A field indicating which of several possible shapes a response takes. Generated clients need it declared. |
| Composition | Building a schema from others — a common base plus specific additions — rather than repeating fields. |
| Security scheme | A declared authentication method, applied document-wide or per operation. |
| Contract testing | Verifying that the implementation conforms to the specification, automatically and repeatedly. |
| Breaking change | Any change that could cause a conforming client to fail. Removal, renaming and type changes always qualify. |
| Linting | Automated checks on the specification's style and completeness, enforcing consistency without debate. |
| Mock | A server returning specification-conforming responses with no implementation behind it. |
Two entries carry disproportionate weight in practice. Reference is what separates a specification that stays correct from one that decays, because a concept defined once cannot diverge from itself. And breaking change is worth agreeing on precisely as a team, because the disagreements are always at the edges — adding an enumeration value, tightening validation, making an optional field required — and those edges are exactly where consumers get broken by accident.
Key takeaways
- The specification is the contract, not a description written afterwards.
- Define once, reference everywhere. Duplicated schemas diverge within a quarter.
- Separate request and response schemas. One shared schema makes everything optional.
- Generate everything from it: docs, types, mocks, validation, tests.
- Enforce it in the pipeline. Lint, detect breaking changes, verify conformance, publish on merge.
- Know its limits. Structure yes; business rules, sequencing and failure semantics belong in prose.
An API with a maintained specification is not merely better documented. It is one where a breaking change cannot happen accidentally, consumers can start before the implementation exists, and nobody has to ask whether the documentation is still true — which removes most of the friction from integrating with anyone.
Enjoyed this article?
Get more engineering insights from ELIVTECH — or talk to us about your project.
Get in touch