API Versioning and Backward Compatibility: Evolving APIs Safely
API Versioning and Backward Compatibility: Evolving APIs Safely
Every API eventually needs to change, and every change risks breaking a client you do not control. The discipline is simple in principle: never break existing clients, and give them a clear path forward. This guide covers versioning strategies, the rules of backward compatibility, and the deprecation lifecycle.
Versioning Strategies
URI Versioning
The most common and most visible approach:
GET /v1/orders/123
GET /v2/orders/123
// Express — URI versioning
app.use("/v1", v1Router);
app.use("/v2", v2Router);
Pros: explicit, cacheable, easy to debug. Cons: URL churn, and it encourages forking entire endpoints instead of evolving them.
Header Versioning
The version lives in a header:
GET /orders/123
Accept: application/vnd.acme.orders.v2+json
// Express — header versioning
app.get("/orders/:id", (req, res) => {
const version = req.headers["accept"]?.match(/v(\d+)/)?.[1] ?? "1";
if (version === "2") return handleV2(req, res);
return handleV1(req, res);
});
Pros: clean URLs, fine-grained control. Cons: invisible in logs and browser devtools, harder to cache, clients must remember to send it.
Query Parameter Versioning
GET /orders/123?version=2
Pros: trivial to implement. Cons: pollutes the URL, easy to forget, and it is the weakest of the three. Fine for internal APIs, poor for public ones.
The Compatibility Rules
Versioning is a fallback. The first line of defense is backward-compatible evolution:
Additive changes are always safe:
- Adding a new optional field to a response
- Adding a new endpoint
- Adding a new enum value (if clients handle unknown values)
- Adding a new optional request parameter
Breaking changes require a new version:
- Removing or renaming a field
- Changing a field's type
- Changing an enum's meaning
- Making an optional parameter required
- Changing error semantics
- Removing an endpoint
// Safe: add an optional field
type OrderV1 = {
id: string;
total: number;
};
// Safe: additive evolution
type OrderV2 = OrderV1 & {
currency: string; // new optional field
};
The Deprecation Lifecycle
A version does not die on a date; it dies on a process. The lifecycle:
- Announce: document the deprecation and the target date.
- Overlap: run old and new versions in parallel for a defined window (typically 6-12 months).
- Measure: track usage of the deprecated version. Deprecate only when usage is near zero.
- Remove: delete the old version, keep the docs.
// Track deprecated version usage
app.use("/v1", (req, res, next) => {
metrics.increment("api.v1.requests", { path: req.path });
res.setHeader("Deprecation", "true");
res.setHeader("Sunset", "Wed, 01 Jan 2027 00:00:00 GMT");
next();
});
The Deprecation and Sunset headers are the standard way to signal deprecation to automated clients.
Compatibility Testing
Backward compatibility is a property you can test. Contract testing (Pact) and schema diffing catch breaking changes in CI:
// Schema diffing with zod — fail CI on breaking changes
import { diff } from "json-schema-diff";
const breaking = diff(v1OrderSchema, v2OrderSchema).filter(
(change) => change.type === "removed" || change.type === "type-changed",
);
if (breaking.length > 0) {
throw new Error(`Breaking change detected: ${JSON.stringify(breaking)}`);
}
# CI job — run contract tests against the published provider
- name: Verify provider contracts
run: npx pact-broker can-i-deploy --pacticipant orders-api --version $GITHUB_SHA
GraphQL: Versioning Is Different
GraphQL has no versions by design: the schema is the contract, and evolution is additive. The rules:
- Add fields, never remove them (until you run a schema migration).
- Deprecate with the
@deprecateddirective:
type Order {
id: ID!
total: Float!
currency: String @deprecated(reason: "Use totalCurrency instead")
totalCurrency: String!
}
- Use schema registry checks to block breaking changes in CI (Apollo Studio, GraphQL Inspector).
# Block breaking schema changes in CI
npx graphql-inspector diff schema.graphql --rule 'no-breaking-changes'
Practical Rules for API Teams
- Default to additive changes. Most "breaking" changes can be done additively with a deprecation window.
- Never silently change semantics. Changing what a field means without changing its shape is the worst kind of break: it fails at runtime, not at compile time.
- Document every version. A version without docs is a trap.
- Version the errors too. Error shapes change; version them with the API.
- Keep old versions alive long enough. The cost of running an old version is small; the cost of breaking a client is reputation.
Implementation Checklist
- Choose a versioning strategy (URI, header, or query)
- Default to additive, backward-compatible changes
- Define the deprecation lifecycle with dates
- Send Deprecation and Sunset headers
- Add schema diffing to CI
- Add contract tests for consumers
- Track deprecated version usage
- Document every version and its changes
MatterAI builds frontier AI infrastructure for engineering teams — from inference-optimized models to autonomous coding agents and agentic code reviews.
Explore what we're building:
- Orbital IDE — Autonomous AI coding agent with background agents and deep codebase memory
- AI Code Reviews — Agentic pre-commit reviews across GitHub, GitLab, and Bitbucket
- Axon Models — Frontier-grade reasoning models at 70% lower inference cost
Share this Guide:
More Guides
Local LLMs in Your IDE: Connecting Ollama to Coding Agents and Autocomplete
Wire local models into VS Code, JetBrains, Cline, Continue, and Aider via the OpenAI-compatible API. Covers model routing, context budgets, tool calling with small models, and when a local model is the right choice for the job.
15 min readBuilding a Self-Hosted AI Stack: Ollama, Open WebUI, and Local RAG
Stand up a fully self-hosted AI stack on a single machine: Ollama for inference, Open WebUI as the chat interface, local embeddings for RAG, and a reverse proxy for secure access. No cloud dependency, no data leaving your network.
17 min readTop 5 Open-Source Coding Models to Run on Your Mac (2026)
The best local coding models for Apple Silicon in 2026, ranked by quality per gigabyte of unified memory. Covers qwen3-coder, devstral, gpt-oss, and more with real pull tags, sizes, and context windows.
14 min readRunning LLMs Locally: GGUF, Quantization, and Memory Planning
Learn the GGUF format, the quantization ladder from Q2 to FP16, and the exact memory math for running models on Apple Silicon and NVIDIA GPUs. Includes Ollama and llama.cpp tuning for KV cache and context.
15 min readOllama vs vLLM vs llama.cpp: Choosing the Right Local LLM Runtime
Compare the three dominant local LLM runtimes on architecture, throughput, hardware, and deployment context. Includes benchmark data, a decision framework, and a migration path from Ollama to vLLM.
16 min readContinue Reading
Local LLMs in Your IDE: Connecting Ollama to Coding Agents and Autocomplete
Wire local models into VS Code, JetBrains, Cline, Continue, and Aider via the OpenAI-compatible API. Covers model routing, context budgets, tool calling with small models, and when a local model is the right choice for the job.
15 min readBuilding a Self-Hosted AI Stack: Ollama, Open WebUI, and Local RAG
Stand up a fully self-hosted AI stack on a single machine: Ollama for inference, Open WebUI as the chat interface, local embeddings for RAG, and a reverse proxy for secure access. No cloud dependency, no data leaving your network.
17 min readTop 5 Open-Source Coding Models to Run on Your Mac (2026)
The best local coding models for Apple Silicon in 2026, ranked by quality per gigabyte of unified memory. Covers qwen3-coder, devstral, gpt-oss, and more with real pull tags, sizes, and context windows.
14 min readShip Faster. Ship Safer.
Join thousands of engineering teams using MatterAI to autonomously build, review, and deploy code with enterprise-grade precision.
