Overview
API versioning is how you change an API without breaking existing clients. Once mobile apps, partners, and other teams depend on an API, removing a field or changing its meaning can break them. Versioning gives clients a stable contract while the API evolves.
Common strategies are a version in the URL path (/v1/orders), a header (Accept or a custom API-Version), a query parameter, and date-based versions (Stripe's 2024-06-20). Equally important is evolving without versions where possible, through additive, backward-compatible changes, and having a clear deprecation process when breaking changes are unavoidable.
A new edition can reorganize chapters, but students in an existing course keep using the edition their syllabus references. Publishers announce when old editions will stop being printed.
When to use it
- Public or partner APIs with clients you do not control.
- Mobile backends where old app versions stay installed for years.
- Making breaking changes to response shapes or semantics.
- Designing an API governance policy.
Where it shows up in interviews
Recognize it when: change an API used by external clients.
- Design a public payments API
- Design a mobile backend with old app versions
Recognize it when: many teams consume internal APIs.
- Design an API platform for microservices
Where it is used in real software
Uses date-based versions pinned per account, with a header override, and maintains old versions for years.
GitHub uses a header-based API version; many APIs, including Twilio's, use versions in URL paths.
Follow AIP guidance with major versions in the path (v1, v2) and backward-compatible minor changes.
Key terms
- Breaking change
- Change that can make existing clients fail (removing or renaming fields, changing types or meaning).
- Backward compatible
- Old clients keep working (adding optional fields, new endpoints).
- Deprecation
- Announcing a version or field will be removed, with a timeline.
- Sunset header
- HTTP header indicating when a resource will stop being available.
- Tolerant reader
- Clients ignore unknown fields so additions do not break them.
How it works, step by step
- 1Prefer additive changes
Add fields and endpoints instead of changing existing ones.
- 2Choose a versioning scheme
Path, header, or date-based; apply it consistently.
- 3Introduce a new version only for breaking changes
Keep the old version running in parallel.
- 4Translate between versions
Map old requests and responses to the new internal model at the edge.
- 5Deprecate with a timeline
Announce, add Deprecation and Sunset headers, measure usage, then remove.
Versioning strategies
Trade-offs
| Strategy | Example | Pros | Cons |
|---|---|---|---|
| URL path | /v2/orders | Visible, easy to route and cache | URLs change; coarse-grained |
| Header | API-Version: 2 | Clean URLs, per-request control | Less visible; harder to test in a browser |
| Query parameter | /orders?version=2 | Easy to try | Easy to forget; caching quirks |
| Date-based | 2024-06-20 | Fine-grained evolution per account | Requires version translation layers |
NOWStrategy: URL path | Example: /v2/orders | Pros: Visible, easy to route and cache | Cons: URLs change; coarse-grained
Path versioning is the most common default; date-based versioning suits APIs with frequent evolution and long-lived integrations.
Implementation
// Internal model evolves; adapters keep old contracts stabletype OrderV2 = { id: string; total: { amount: number; currency: string }; status: "pending" | "paid" }; const toV1 = (o: OrderV2) => ({ id: o.id, total_cents: o.total.amount, status: o.status }); // old shape app.get("/v1/orders/:id", async (req, res) => { const order = await orders.get(req.params.id); res.set({ Deprecation: "true", Sunset: "Wed, 31 Dec 2026 23:59:59 GMT", Link: '</v2/orders>; rel="successor-version"' }); res.json(toV1(order));}); app.get("/v2/orders/:id", async (req, res) => { res.json(await orders.get(req.params.id));});Complexity and performance
Each version costs testing and maintenance.
Mobile and partner integrations change slowly.
Trade-offs
Supporting many versions protects clients but multiplies code paths and tests; translation layers at the edge reduce the cost.
Major path versions are simple but force big migrations; date-based or per-field evolution is smoother but more complex to implement.
Variants and related techniques
GraphQL avoids versions by adding fields and deprecating old ones with @deprecated.
Tools like Pact verify providers do not break consumers.
Common mistakes
- Changing field meaning without a version.
Fix: Semantic changes are breaking changes even if the type is the same.
- Removing versions without measuring usage.
Fix: Track traffic per version and contact remaining clients before sunset.
- Clients that fail on unknown fields.
Fix: Encourage tolerant readers so additive changes stay safe.
Interview questions
How would you version a public REST API?
Evolve additively whenever possible, use a clear scheme (often /v1 in the path, or date-based versions for fine-grained evolution), keep old versions running with translation adapters, and deprecate with announced timelines, Deprecation and Sunset headers, and usage monitoring.
What counts as a breaking change?
Removing or renaming fields or endpoints, changing types or formats, making optional fields required, changing default behavior or error codes, and changing the meaning of existing values.
Practice problems
| Problem | Difficulty | What it trains |
|---|---|---|
| Classify 10 API changes as breaking or not | Easy | Compatibility. |
| Design a deprecation plan for v1 to v2 | Medium | Migration and communication. |