Every API changes over time. New features are added, data structures evolve, endpoints are redesigned, and performance optimizations require response format changes. Without a versioning strategy, every change risks breaking the mobile apps, websites, and integrations that depend on your API. One unannounced field rename can crash thousands of client applications instantly.
API versioning solves this by letting you introduce changes in a new version while keeping the old version running. Existing clients continue working on v1 while new clients adopt v2. This guide covers every versioning strategy, when to version, what constitutes a breaking change, how to deprecate, and practical implementation across multiple frameworks.
What Is API Versioning?
API versioning is the practice of assigning version identifiers (v1, v2, v3) to different iterations of your API. Each version represents a stable contract — clients that integrate with v1 know exactly what request format to send and what response to expect, regardless of what v2 or v3 look like.
Real-world analogy: Think of textbook editions. The 3rd edition of a textbook has updated content, but students who bought the 2nd edition can still use it — the bookstore stocks both. Eventually the 1st edition goes out of print (deprecated), but it doesn't happen overnight. API versioning follows the same principle.
Without versioning, you are forced to either: never change anything (impossible for evolving products), or change everything and break all existing clients simultaneously (unacceptable for production APIs with real users).
When Should You Create a New API Version?
Not every change requires a new version. The key question: "Will this change break existing clients?" If yes, it needs a new version. If no, it can go into the current version.
Breaking Changes (Require New Version)
Removing a field from the response
Removing "phone" from user response — clients reading user.phone will get undefined
Renaming a field
Changing "userName" to "username" — clients accessing response.userName break
Changing a field's data type
Changing "age" from number to string — clients doing age + 1 get "301" instead of 31
Removing an endpoint
Deleting DELETE /users/:id — clients calling it get 404
Changing authentication method
Moving from API key to OAuth — all existing integrations fail immediately
Making optional parameters required
Requiring "email" on POST /users — clients that don't send it get 400
Changing error response format
Changing {error: "msg"} to {errors: [{msg: "..."}]} — client error handling breaks
Non-Breaking Changes (No New Version Needed)
Adding new optional fields to response
Adding "avatar_url" — old clients simply ignore fields they don't know
Adding new endpoints
Adding GET /users/:id/preferences — existing endpoints are unchanged
Adding optional query parameters
Supporting ?include=posts — clients that don't use it get the same response as before
Improving performance (same response)
Faster database query — response is identical, just arrives sooner
Adding new enum values
Adding "suspended" to user status — clients should handle unknown values gracefully
API Versioning Strategies
1. URL Path Versioning (Most Popular)
The version number is part of the URL path. This is the most widely used approach — GitHub, Stripe, Google, and Twitter all use it. Its main advantage: the version is immediately visible, obvious to developers, cacheable by CDNs, and requires zero special configuration in clients.
GET /api/v1/users → Version 1
GET /api/v2/users → Version 2
GET /api/v3/users/123 → Version 3
// Express.js implementation
const v1Router = express.Router()
const v2Router = express.Router()
v1Router.get("/users", getUsers_v1)
v2Router.get("/users", getUsers_v2)
app.use("/api/v1", v1Router)
app.use("/api/v2", v2Router)
// Client usage (explicit, no ambiguity)
fetch("https://api.example.com/v2/users")2. Custom Header Versioning
The version is specified in a custom HTTP header. This keeps URLs "clean" (no version in the path) but makes the version invisible in browser address bars and harder to test without tools like Postman. It's more common for internal APIs where all clients are controlled.
// Request with version header
GET /api/users
API-Version: 2
// or: X-API-Version: 2
// Express.js middleware
function versionMiddleware(req, res, next) {
req.apiVersion = parseInt(req.headers["api-version"]) || 1
next()
}
app.get("/api/users", versionMiddleware, (req, res) => {
if (req.apiVersion === 2) return getUsers_v2(req, res)
return getUsers_v1(req, res)
})3. Accept Header Versioning (Content Negotiation)
The version is embedded in the Accept header using a custom MIME type. This is the most "RESTful" approach in theory (using content negotiation for versioning) but the least developer-friendly in practice — the syntax is complex and unfamiliar to most developers.
// Accept header versioning (GitHub uses this) GET /users/123 Accept: application/vnd.example.v2+json // Breaking down the MIME type: // application/ → category // vnd.example. → vendor namespace // v2 → version // +json → format // GitHub's actual approach: Accept: application/vnd.github.v3+json
4. Query Parameter Versioning
The version is passed as a query parameter. This is simple to implement but has downsides: it's optional (clients can forget it), mixes versioning with business parameters, and complicates caching. Used less frequently in practice.
// Query parameter versioning GET /api/users?version=2 GET /api/users?api-version=2024-01-15 (date-based, used by Azure) // Downside: easy to forget, defaults to latest (dangerous) GET /api/users → Which version? Depends on server default.
Versioning Strategies Compared
| Feature | URL Path | Header | Accept | Query Param |
|---|---|---|---|---|
| Popularity | ★★★★★ (most used) | ★★★ | ★★ | ★★ |
| Visibility | Obvious in URL | Hidden in headers | Hidden in headers | Visible in URL |
| CDN Caching | Easy (different URLs) | Needs Vary header | Needs Vary header | Easy (different URLs) |
| Browser testing | Just change URL | Needs extension/tool | Needs extension/tool | Just add param |
| Tool support | Universal | Good (Postman, etc.) | Limited | Universal |
| RESTfulness | Moderate (version in URI) | Good | Most RESTful | Poor (mixed concerns) |
| Client complexity | Very simple | Must set header | Complex MIME type | Simple |
| Best for | Public APIs | Internal APIs | API purists | Simple/legacy APIs |
API Deprecation Strategy
Deprecation is the process of phasing out an old API version responsibly. You never just "turn off" a version without warning — that breaks clients and destroys trust. A proper deprecation follows a timeline:
Month 0: Announce deprecation
Blog post, email, changelog, API docs update. New version is available.
Month 1-6: Migration period
Old version still works. Deprecation header sent. Documentation shows migration path.
Month 6-9: Sunset warnings
Sunset header with removal date. Direct outreach to remaining high-traffic consumers.
Month 12: Removal
Old version returns 410 Gone with migration instructions in the body.
// Deprecation response headers
HTTP/1.1 200 OK
Deprecation: true
Sunset: Sat, 01 Jan 2027 00:00:00 GMT
Link: <https://api.example.com/v2/users>; rel="successor-version"
// After sunset date:
HTTP/1.1 410 Gone
Content-Type: application/json
{
"error": "This API version has been removed",
"message": "Please migrate to v2",
"documentation": "https://docs.example.com/migration-guide",
"successor": "https://api.example.com/v2/users"
}Common API Versioning Mistakes
⚠️ No versioning at all
Launching without versioning means your first breaking change has no safe path forward. Adding versioning later requires all clients to update simultaneously — a logistical nightmare.
✅ Fix: Add versioning from v1 launch. Even if you never need v2, the cost is trivial and the safety net is invaluable.
⚠️ Removing endpoints without deprecation period
Deleting an endpoint immediately breaks every client using it. Mobile apps can't be force-updated — users on old versions crash until they update.
✅ Fix: Follow a 6-12 month deprecation timeline. Return 410 Gone (not 404) with migration instructions after sunset.
⚠️ Too many active versions (v1, v2, v3, v4, v5...)
Each version is code you maintain, test, deploy, and document. Five active versions means five times the maintenance burden, bug surface, and cognitive load.
✅ Fix: Support maximum 2-3 versions simultaneously. Aggressively deprecate old versions. Avoid creating new versions for non-breaking changes.
⚠️ Breaking changes within a version
Adding a required field to v1 after clients are already using it is a breaking change — even though you didn't create v2. Clients trusted the v1 contract.
✅ Fix: Once a version is published, its contract is immutable. New required fields, removed fields, or structural changes always go in a new version.
⚠️ Defaulting to latest version when no version specified
If a client doesn't specify a version and you default to 'latest,' their code breaks every time you release a new version without them changing anything.
✅ Fix: Either require version specification (reject versionless requests) or default to a specific stable version (not 'latest').
⚠️ Poor documentation of version differences
Clients can't migrate if they don't know what changed. Without a changelog and migration guide, developers have to guess what's different between v1 and v2.
✅ Fix: For every version bump, publish: what changed, why it changed, how to migrate, and a code diff example.
Best Practices
Don't create v2 for every small feature. Non-breaking additions (new optional fields, new endpoints) go into the current version.
It's the most explicit, widely understood, and tool-friendly approach. Developers see the version immediately in every request.
A published version is a contract. Never change its behavior, even to fix bugs — unless the bug is a security vulnerability.
Give clients adequate time to migrate. Enterprise clients may have quarterly release cycles — they need time.
Publish what changed, what was removed, what was added, and how to migrate. Include before/after code examples.
Design responses that can grow: use objects (not primitives), make fields optional, use enums that allow unknown values.
Deprecation: true and Sunset: <date> headers warn clients programmatically — they can automate migration alerts.
Track how many requests each version receives. When v1 drops below 1% of traffic, it's safe to sunset.
Make migration as easy as possible. Provide scripts, SDK updates, and step-by-step guides — not just a changelog.
Once clients integrate with v1, that contract is sacred. Any change that could break even one client belongs in v2.
Versioning Decision Guide
| Scenario | Recommended Strategy | Why |
|---|---|---|
| Public REST API | URL path (/api/v2/) | Most explicit, universally understood, CDN-cacheable |
| Internal API (same team) | Header or no versioning | Less overhead, team controls all consumers |
| Enterprise API (partners) | URL + deprecation policy | Partners need clear contracts and migration time |
| Microservices | API Gateway + URL versioning | Gateway routes to correct service version |
| Mobile backend | URL versioning | Old app versions can't be force-updated — must support old API |
| SaaS product API | URL + date-based changelog | Clear evolution history for customers |
Frequently Asked Questions
What is API versioning?
Why do APIs need versioning?
Which API versioning strategy is best?
What is a breaking change in an API?
What is backward compatibility?
What is API deprecation?
Should internal APIs be versioned?
How long should old API versions be supported?
Can an API have multiple versions running simultaneously?
URL versioning vs Header versioning — which is better?
Related Articles & Tools
Conclusion
API versioning is the safety net that lets your API evolve without breaking the applications that depend on it. The key principles: version only for breaking changes (not every feature), use URL path versioning for public APIs (most explicit and tool-friendly), maintain a clear deprecation timeline (6-12 months minimum), document every change with migration guides, and design your API to minimize the need for breaking changes in the first place.
The best versioning strategy is the one you implement from day one. Adding versioning to an existing API is painful — adding it at launch is trivial. Start with /api/v1/ and you'll have a clean path forward when v2 inevitably becomes necessary.
Remember: your API version is a promise to your consumers. Breaking that promise — whether through unannounced changes, surprise deprecations, or insufficient migration time — destroys the trust that makes your API valuable. Version responsibly.
