Running API Design Reviews: Contracts, Backward Compatibility, and Change Management
A practical guide to establishing API design reviews as a team engineering practice, covering proposal docs, approval gates, OpenAPI contract-first development, automated compatibility checking in CI, and scaling the process from a 5-person team to a platform org.
Code reviews catch implementation bugs. Architecture reviews validate structural decisions. API design reviews do something neither of those does: they protect your consumers from future versions of yourself.
An API is a promise. The moment another system starts consuming an endpoint, you have taken on an obligation. Code reviews almost never surface this obligation explicitly. A PR that renames a JSON field from user_id to userId passes code review because the code compiles, the tests pass, and the change looks like a style cleanup. It breaks production for every consumer that is reading user_id from the response.
The teams that learn this lesson the hard way tend to learn it at the worst possible moment: during a mobile app release, on a Friday afternoon, with a partner integration that you cannot easily contact. API design reviews exist to catch these problems before they become production incidents.
This guide covers how to run them.
Why API Reviews Are Higher Leverage Than Code Reviews
Code reviews catch bugs within a change. API reviews catch problems that will exist in the contract for years.
The asymmetry matters. A bad implementation can be refactored. A bad API shape cannot be changed without a versioning event and a consumer migration, which costs orders of magnitude more than writing the shape correctly in the first place. Every field name you publish, every pagination scheme you choose, every error code you return becomes load-bearing the moment a consumer depends on it.
Three categories of API mistakes that code review will almost never catch:
Semantic narrowing. An endpoint returns a status field as a string. The valid values today are active and inactive. In six months, the business needs suspended and pending_review. If consumers were written as if status === 'active' rather than if status !== 'inactive', adding new values silently changes behavior for all of them. A design review asks: what is the full state space? Will it grow?
Implicit coupling. An endpoint returns a list of items sorted by createdAt descending. The API spec says nothing about sort order. Consumer A builds pagination logic that depends on stable ordering. Consumer B displays items sorted by position assuming the API will always return them in display order. The sort order was never a documented contract, but it became one. A design review forces the author to state what is and is not guaranteed.
Premature exposure. A field that represents internal processing state gets included in the response because it is useful for debugging. Consumers start depending on it. Now it cannot be removed. A design review asks whether each field is intentional enough to be public.
The Review Process: Proposal, Meeting, Approval
The mechanical structure of an effective API design review process has three stages.
Stage 1: The API Proposal Document
Before any code is written, the API author produces a short design document. Two pages is enough for most changes. The document should cover:
Problem statement. What consumer need does this API satisfy? Why does the existing API surface not meet it?
Resource model. The proposed JSON shapes for request and response, written out explicitly. Not as pseudocode. As actual typed structures.
Behavior specification. What HTTP methods, what status codes, what error conditions? What happens on partial failure? Is this operation idempotent?
Consumer impact analysis. Who will use this API? Are there existing consumers that will be affected by any changes to existing endpoints?
Evolution considerations. What is likely to change about this API in the next 12 months? Are there design decisions today that will make those future changes painful?
The proposal document serves a purpose beyond the review itself: it forces the author to think through the design before writing code. Engineers who skip the document and write the code first tend to arrive at reviews defending their implementation rather than evaluating their design. The separation matters.
Stage 2: The Review Meeting (or Async Thread)
For straightforward additions to existing APIs, an async review thread in a PR is usually enough. The PR should contain the updated OpenAPI spec, the proposal document, and nothing else. No implementation code. Reviewing implementation code alongside the API design shifts attention to the wrong thing.
For new APIs, new resources, or any change that touches an existing contract, a synchronous session is worth the time. Keep it to 45 minutes. The participants: the API author, at least one engineer who will consume the API, and a designated API steward if your team has one.
The review session agenda:
- Author walks through the resource model (10 minutes). No presentation, just walking the document.
- Consumer perspective: what is missing, what is unclear, what will be painful to use (15 minutes). The consumer engineer is the most valuable voice in the room.
- Open discussion on edge cases: error states, partial failures, empty collections, pagination boundaries (10 minutes).
- Explicit decision on backward compatibility: is this additive, does it require a version bump, does it touch an existing contract? (10 minutes).
Document every decision and the rationale. The meeting notes become part of the ADR (Architecture Decision Record) for the API. Engineers joining the team two years later will want to know why the endpoint was designed the way it was.
Stage 3: Approval Gates
The gate structure depends on the risk of the change:
| Change type | Required approvals | Async or sync |
|---|---|---|
| New endpoint on existing v1 | 1 API steward + 1 consumer | Async PR |
| New resource (new path prefix) | 1 API steward + 1 consumer + team lead | Sync session |
| Modification to existing contract | 1 API steward + all known consumers notified | Sync session + deprecation plan |
| Breaking change with version bump | API steward + consumer lead + migration plan approved | Sync session |
The “all known consumers notified” requirement for contract modifications is the gate most teams skip and most regret skipping. Notification does not mean approval. It means everyone with a dependency on the contract has seen the proposed change and had an opportunity to raise concerns before it is merged.
Contract-First Development with OpenAPI
Contract-first means the OpenAPI spec is written and approved before the implementation begins. The spec is the deliverable of the review process. The implementation follows from it.
This is the opposite of the common pattern where engineers write code, then run a spec generator against the running server to produce documentation. Spec-from-code documentation is often missing descriptions, uses internal naming that leaked into the API shape, and has no guarantee of intent since the spec was never reviewed.
A useful project structure for contract-first development:
/api-contracts
/v1
openapi.yaml # source of truth
/schemas # reusable $ref components
user.yaml
order.yaml
error.yaml
/v2
openapi.yaml
/src
/routes # implementation follows the spec
The spec lives in a separate directory (or a separate repository for larger orgs) from the implementation. This makes it easier to review spec changes in isolation and harder to accidentally drift the implementation away from the contract.
Here is a realistic OpenAPI spec fragment that shows what a reviewed spec looks like compared to a generated one. The critical difference is the description fields, the explicit enum documentation, and the documented error responses:
# api-contracts/v1/openapi.yaml
paths:
/orders/{orderId}:
get:
operationId: getOrder
summary: Retrieve a single order by ID
description: |
Returns the full order record. The `status` field represents
the order's lifecycle state. Note that `items` is always returned
as an array even for single-item orders. Sort order of items
is not guaranteed — do not rely on insertion order.
parameters:
- name: orderId
in: path
required: true
schema:
type: string
format: uuid
responses:
"200":
description: Order found
content:
application/json:
schema:
$ref: "#/components/schemas/Order"
"404":
description: Order not found
content:
application/json:
schema:
$ref: "#/components/schemas/ApiError"
example:
code: ORDER_NOT_FOUND
message: "No order exists with the given ID"
requestId: "req_01HXYZ123"
"403":
description: Caller does not have access to this order
content:
application/json:
schema:
$ref: "#/components/schemas/ApiError"
example:
code: ORDER_ACCESS_DENIED
message: "You do not have permission to view this order"
requestId: "req_01HXYZ124"
components:
schemas:
Order:
type: object
required: [id, status, createdAt, items]
properties:
id:
type: string
format: uuid
status:
type: string
enum: [pending, processing, shipped, delivered, cancelled]
description: |
Order lifecycle state. Terminal states: delivered, cancelled.
New states may be added in future API versions — consumers
must handle unknown status values gracefully.
createdAt:
type: string
format: date-time
description: ISO 8601 timestamp in UTC
items:
type: array
minItems: 0
items:
$ref: "#/components/schemas/OrderItem"
Notice the explicit callout in the status description: “New states may be added in future API versions - consumers must handle unknown status values gracefully.” That single sentence is the contract clause that prevents the semantic narrowing problem described earlier. It communicates intent, sets expectations for consumers, and documents the future evolution plan.
Automated Compatibility Checking in CI
Human review catches design problems. Automated checks catch regressions that slip through human review. You need both.
The two tools that belong in every API CI pipeline:
Spectral for style and convention enforcement. oasdiff (or openapi-diff) for breaking change detection.
Spectral Linting
Spectral evaluates your OpenAPI spec against a ruleset. Your ruleset should encode your style guide decisions so that violations are caught automatically:
// .spectral.mjs
import { oas } from "@stoplight/spectral-oas";
export default {
extends: [oas],
rules: {
// Every property must have a description
"oas3-schema-descriptions": {
message: "Schema properties must include a description",
given: "$.components.schemas[*].properties[*]",
then: {
field: "description",
function: "defined",
},
severity: "error",
},
// Status fields must include an enum (open-ended strings are a design smell)
"no-untyped-status-fields": {
message: "Fields named 'status' must define an enum",
given: "$.components.schemas[*].properties.status",
then: {
field: "enum",
function: "defined",
},
severity: "warn",
},
// Error responses must include the standard error schema
"error-response-shape": {
message: "4xx and 5xx responses must reference the ApiError schema",
given: "$.paths[*][*].responses[4*,5*].content['application/json'].schema",
then: {
field: "$ref",
function: "pattern",
functionOptions: {
match: "ApiError",
},
},
severity: "error",
},
},
};
Run Spectral in CI on every PR that modifies any file under api-contracts/:
# .github/workflows/api-review.yml
name: API Contract Review
on:
pull_request:
paths:
- "api-contracts/**"
jobs:
spectral-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Spectral
run: npm install -g @stoplight/spectral-cli
- name: Lint OpenAPI spec
run: spectral lint api-contracts/v1/openapi.yaml --fail-severity error
breaking-change-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install oasdiff
run: |
curl -fsSL https://raw.githubusercontent.com/oasdiff/oasdiff/main/install.sh | sh
- name: Check for breaking changes
run: |
oasdiff breaking \
origin/main:api-contracts/v1/openapi.yaml \
api-contracts/v1/openapi.yaml \
--fail-on ERR
The --fail-on ERR flag in oasdiff tells it to exit non-zero only for breaking changes, not for warnings (like deprecated fields). This is intentional: warnings can be reviewed by humans, but breaking changes should block the PR until a designated reviewer explicitly approves the exception.
What Automated Checks Will Not Catch
Automated checks are reliable for structural violations. They do not catch semantic problems. oasdiff will tell you that you removed a field. It will not tell you that the field you added means something different from the field you left in place. It will not notice that you changed sort order without updating the spec. Human review covers what automation cannot.
Real API Changes That Broke Clients
Abstract discussions about backward compatibility are easy to ignore. Concrete examples are harder to dismiss.
The pagination type change. An API returned { totalCount: 1523 } as a JSON number. At 2.1 billion records, the team changed the field to a string to accommodate the scale. The spec was updated. The code was reviewed. CI passed. Two mobile clients that had typed the field as Int in their Swift models silently discarded values above Int32.MAX with no error. The bug was in production for eleven days before a data anomaly surfaced it in a report.
What a design review would have caught: the evolution consideration question. “What is the largest value this field could realistically reach?” If the answer is “billions,” the field should have been a string from day one.
The enum removal. An order management API had a status value of on_hold that was used internally. The business decided to retire the on_hold workflow. The team removed on_hold from the enum in the spec and stopped returning it in responses. No breaking change detector flagged it because they were using a home-grown diff script that only checked for field removals, not enum value removals.
A third-party logistics partner had a switch statement on order status. Their default case logged and silently skipped unrecognized statuses. Orders that arrived while they were mid-migration were silently dropped. The total count was 47 orders across 8 days before the discrepancy showed up in reconciliation.
What a design review would have caught: the explicit question “who are the known consumers, and have they confirmed this enum value is safe to remove?” Also: oasdiff would have flagged the enum value removal as a breaking change.
The response field rename. A backend engineer renamed created_at to createdAt as part of a camelCase standardization effort. The endpoint was not versioned. The PR title was “style: standardize field naming to camelCase.” The code review approved it. The API governance document said “use camelCase” so the change looked like it was fixing a violation, not introducing one.
Three internal consumers broke silently because JavaScript’s property access on undefined does not throw by default. The consumers read response.created_at, got undefined, and continued processing. The breakage surfaced in timestamp-dependent business logic across three different services over the following two weeks.
What a design review would have caught: any change to an existing field name is a breaking change regardless of how stylistically correct the new name is. The approval gate for modifying an existing contract would have required all consumers to be notified and confirm readiness.
Managing Breaking Changes When They Are Unavoidable
Sometimes a breaking change is the right call. The field model was fundamentally wrong, and the cost of carrying it forward exceeds the cost of a migration. The process for handling an intentional breaking change:
Step 1: Issue a sunset header on the current endpoint. Add Sunset and Deprecation headers immediately when the decision is made. Do not wait until the v2 endpoint is ready. Consumers who instrument their HTTP layer will catch the notice automatically.
// Express middleware for deprecation headers
function deprecationMiddleware(sunsetDate: string, migrationGuide: string) {
return (req: Request, res: Response, next: NextFunction) => {
res.setHeader("Deprecation", "true");
res.setHeader("Sunset", new Date(sunsetDate).toUTCString());
res.setHeader(
"Link",
`<${migrationGuide}>; rel="successor-version"`
);
next();
};
}
// Apply to deprecated routes
app.get(
"/v1/orders/:id",
deprecationMiddleware("2026-10-01", "https://docs.example.com/migration/v1-to-v2"),
getOrderHandler
);
Step 2: Produce a migration guide before announcing the sunset. The guide should list: every field that was removed, every field that changed type, every field that was renamed, and the v2 equivalent for each. Do not make consumers figure out the mapping themselves.
Step 3: Track consumer migration by monitoring deprecated endpoint traffic. Aggregate calls to deprecated endpoints by consumer identifier. Do not complete the sunset until you have zero traffic or explicit confirmation from every consumer that they have migrated.
Step 4: Set the SDK version at the migration boundary. If you publish client SDKs, bump the major version at the API version boundary. The SDK changelog should reference the migration guide. Consumers upgrading the SDK get the migration guide as part of the upgrade path.
Scaling the Process: From 5 Engineers to Platform Organization
The process described above is calibrated for a team with one or two APIs and a handful of consumers. As the organization grows, different pressures emerge.
At 5-10 engineers: The review is lightweight. One API steward (often the tech lead), async PR review, and a shared understanding that “modifying existing contracts requires an explicit conversation.” The overhead is minimal. The value is in building the habit.
At 15-30 engineers across 2-3 teams: Introduce a dedicated API steward role - a rotating responsibility, not a bottleneck. The steward reviews all spec PRs, maintains the style guide, and tracks the deprecation backlog. Automated CI checks (Spectral + oasdiff) handle the mechanical layer. The steward focuses on design quality.
At 50+ engineers with external consumers: The process formalizes. A designated API council (3-5 senior engineers from different product areas) reviews any new API or breaking change. The council meets weekly. Proposals go in the week before the meeting for async pre-review. The council meeting is a 60-minute decision session, not a first-read session. Decisions are documented as ADRs linked from the spec.
At this scale, the consumer notification requirement becomes a formal RFC process. A proposal to make a breaking change is published internally 30 days in advance. Teams that consume the API have 14 days to raise concerns. The remaining time is the migration window.
| Scale | Review structure | Automation | Deprecation SLA |
|---|---|---|---|
| 5-10 engineers | Async PR + one reviewer | Spectral lint | 30 days, informal |
| 15-30 engineers | Async PR + API steward | Spectral + oasdiff | 60 days, documented |
| 50+ engineers (internal APIs) | Proposal doc + council review | Spectral + oasdiff + consumer traffic dashboards | 90 days, tracked |
| External/partner APIs | RFC process + public changelog | Full toolchain + SDK versioning | 180+ days, contractual |
The column that matters most at every scale is deprecation SLA. It is the enforcement mechanism that gives the review process teeth. A review process without a deprecation policy is feedback without accountability.
Production Considerations
Run compatibility checks against production traffic, not just spec diffs. A field can be “present in the spec” but never actually returned in certain code paths. Shadow-log API responses in staging, run a schema validator against actual response payloads, and surface fields that are in the spec but not in real traffic (good candidates for cleanup) and fields that are in real traffic but not in the spec (undocumented contracts that will surprise you later).
Build a consumer registry. Maintain a list of every known consumer of every API, with the team or individual responsible for each. This registry is what you consult when you need to notify consumers of a deprecation. Without it, “notify all consumers” means “send an email to a general engineering list and hope.”
Version your error codes, not just your resource schemas. Error codes are contracts too. Removing PAYMENT_GATEWAY_TIMEOUT because you renamed it to UPSTREAM_TIMEOUT breaks any consumer that is branching on that specific code. Treat error code registries with the same versioning discipline as response schemas.
Test consumer behavior on unknown enum values in CI. Write tests that explicitly verify your consumers handle unexpected enum values gracefully. Inject a synthetic status value (unknown_future_state) and confirm the consumer does not crash, does not silently drop the record, and logs the unknown value for monitoring. If you cannot pass this test today, you will fail it when the API adds a new status value you did not anticipate.
The goal of an API design review process is not to slow down shipping. It is to move the cost of API mistakes from production incidents and multi-team migration projects to a 45-minute design session and a spec PR. The teams that get this right review the design before writing the code, run automated compatibility checks on every spec change, and treat deprecation as an operation with a defined lifecycle. The teams that skip it spend their time managing the consequences instead of building the next feature.
The process described here costs roughly two to four hours per week of review time at a team of fifteen engineers. The alternative is an API surface that compounds in complexity with every quarter, where the cost of every change is measured in consumer migrations and incident calls. The math is straightforward.
More in Engineering Management
The AI Productivity Paradox: Why Your Team Ships More Code but Delivers Less
AI coding tools create an illusion of velocity at the individual level while degrading team-level delivery, quality, and maintainability. The core mechanism is a 5x+ senior/junior productivity split that aggregate metrics hide entirely.
The AI Productivity Paradox: Why Your Team Ships More Code but Delivers Less Value
93% of developers use AI coding tools, yet DORA metrics haven't improved proportionally. Individual output rises while bug rates, review times, and deployment instability climb. Here is why individual AI productivity gains create organizational drag, and how to fix it with architecture-level guardrails.
Why Your Engineering Team Is Shipping Slower Than 6 Months Ago
Engineering velocity declines at seed-to-Series-A startups for predictable, diagnosable reasons. Process debt, unclear ownership, hiring mistakes, burnout, and architectural bottlenecks all compound. Here is a diagnostic framework you can run in one afternoon, plus a tradeoffs table for each intervention.
The AI Ratchet Effect: Why Giving Your Engineering Team AI Tools Made Them Work Harder, Not Smarter
67% of engineers who adopted AI tools in 2025 worked more hours by year-end, not fewer. This is the AI ratchet effect: management converts every productivity gain into a permanently higher baseline. Here is how it happens, why it is worse at startups, and what a sustainable AI adoption cadence actually looks like.