Quick answer: Breaking API changes are rarely the result of someone intending to break clients; they are the result of a change that looks harmless in a pull request but removes or alters a field that a generated client depends on. Relying on a human review of a large OpenAPI diff is how these slip through, because the breakage is a structural comparison against the previous contract, not a judgment about the code quality. The reliable approach is to run an automated breaking-change check in CI on every spec pull request, classify the delta against the previously released version, and block merges that remove or change published operations without an explicit, reviewed exception. If the first time a breaking change is detected is a client 400 in production, the review process failed; the diff should be rejected automatically long before it ships.
Documentation-focused tools are good at rendering and linting a spec, and linting catches style and structural problems inside the current document, but it cannot answer the separate question of whether this version is compatible with the last released one. That comparison is temporal: it requires two versions of the contract and a rule set for what constitutes a breaking delta, such as deleting a field, tightening a constraint, or making an optional parameter required. A clean, lint-passing spec can still be a breaking change relative to what clients already consume.
What actually counts as breaking
Some changes are obviously breaking and others are quietly so, and the second category is what the CI check is for.
| Change | Breaking? | Why clients break |
|---|---|---|
| Remove an endpoint or field | Yes | Generated calls fail |
| Make optional field required | Yes | Old requests rejected |
| Add an optional field | No | Ignored by old clients |
| Narrow an enum or range | Yes | Valid values now fail |
| Change a type or format | Yes | Parsing assumptions break |
| Add a new endpoint | No | Purely additive |
The enum narrowing row is a classic blind spot: removing a value that existing clients still send passes linting and looks like cleanup, but it turns previously valid requests into 400s the moment the change deploys.
Run the comparison in CI against the released contract
The check should fetch the last released spec, compare it to the pull request version, and fail the build on any breaking rule rather than merely printing a report that a reviewer can ignore.
Wiring the check to the required status means a structural break cannot be merged by accident, which is the whole point. The reviewer then sees an explicit list of breaking rules rather than having to reconstruct the delta from a raw diff.
Allow deliberate breaks through an explicit path
Some breaking changes are intentional, such as a versioned API removal that was announced in advance. Those should not be silently force-merged; they should pass through a documented exception that records the approval and the migration path, so the audit trail shows the break was deliberate and clients were warned.
An expiration on the suppression prevents it from becoming a permanent hole, and tying it to a specific path keeps the rest of the spec protected. The default remains to block; the exception is narrow and time-bound.
Gate client generation and release on the same contract
Because clients and mocks are generated from the same OpenAPI contract, a passing compatibility check means the generated SDKs stay safe to publish, and the diff output can feed release notes automatically. The contract is the single gate: lint for internal correctness, breaking-change detection for backward compatibility, then generation. Keeping these steps together in one pipeline prevents the situation where docs render correctly but the underlying change silently invalidates every consumer.
The practical recommendation is to treat backward compatibility as an automated build gate, not a review expectation. Every spec pull request diffs against the last released contract, breaking rules fail the build, and intentional removals pass through a scoped, expiring suppression with a recorded migration. If detection depends on a reviewer noticing a deleted enum value in a thousand-line diff, clients will find the break first.
Frequently asked questions
Is linting the spec enough to prevent breaks?
No. Linting validates the current document's structure and style, while breaking-change detection compares two versions over time. A lint-clean spec can still remove a field that existing clients use.
What counts as a breaking change?
Removing operations or fields, making an optional parameter required, narrowing enums or ranges, and changing types or formats. Additions of optional fields or new endpoints are generally safe.
How do we ship an intentional breaking change?
Use a scoped, expiring suppression tied to the specific path with a recorded approval and migration guide, rather than disabling the check or force-merging.
Where should the check run?
In CI on every spec pull request, comparing against the last released contract and blocking the merge when a breaking rule fires.
How does this protect generated clients?
Since SDKs and mocks are generated from the same contract, a passing compatibility gate guarantees the published clients remain safe, and the diff can feed release notes automatically.
This is part of the Powerduck vs Redocly comparison — start there for the full picture of contract governance.
Top comments (0)