API Versioning Best Practices

Complete guide — strategies, breaking changes, deprecation, and implementation with code examples

API DesignJune 28, 202618 min readBy Keyur Patel

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

FeatureURL PathHeaderAcceptQuery Param
Popularity★★★★★ (most used)★★★★★★★
VisibilityObvious in URLHidden in headersHidden in headersVisible in URL
CDN CachingEasy (different URLs)Needs Vary headerNeeds Vary headerEasy (different URLs)
Browser testingJust change URLNeeds extension/toolNeeds extension/toolJust add param
Tool supportUniversalGood (Postman, etc.)LimitedUniversal
RESTfulnessModerate (version in URI)GoodMost RESTfulPoor (mixed concerns)
Client complexityVery simpleMust set headerComplex MIME typeSimple
Best forPublic APIsInternal APIsAPI puristsSimple/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

Version only when necessary (breaking changes)

Don't create v2 for every small feature. Non-breaking additions (new optional fields, new endpoints) go into the current version.

Use URL versioning for public APIs

It's the most explicit, widely understood, and tool-friendly approach. Developers see the version immediately in every request.

Keep versions stable once published

A published version is a contract. Never change its behavior, even to fix bugs — unless the bug is a security vulnerability.

Support old versions for 6-12 months minimum

Give clients adequate time to migrate. Enterprise clients may have quarterly release cycles — they need time.

Document every version with changelogs

Publish what changed, what was removed, what was added, and how to migrate. Include before/after code examples.

Minimize breaking changes with extensible design

Design responses that can grow: use objects (not primitives), make fields optional, use enums that allow unknown values.

Send deprecation headers on old versions

Deprecation: true and Sunset: <date> headers warn clients programmatically — they can automate migration alerts.

Monitor version usage metrics

Track how many requests each version receives. When v1 drops below 1% of traffic, it's safe to sunset.

Provide migration guides and codemods

Make migration as easy as possible. Provide scripts, SDK updates, and step-by-step guides — not just a changelog.

Never break the v1 contract

Once clients integrate with v1, that contract is sacred. Any change that could break even one client belongs in v2.

Versioning Decision Guide

ScenarioRecommended StrategyWhy
Public REST APIURL path (/api/v2/)Most explicit, universally understood, CDN-cacheable
Internal API (same team)Header or no versioningLess overhead, team controls all consumers
Enterprise API (partners)URL + deprecation policyPartners need clear contracts and migration time
MicroservicesAPI Gateway + URL versioningGateway routes to correct service version
Mobile backendURL versioningOld app versions can't be force-updated — must support old API
SaaS product APIURL + date-based changelogClear evolution history for customers

Frequently Asked Questions

What is API versioning?
API versioning is the practice of managing changes to an API by assigning version numbers to different iterations. It allows developers to evolve the API while maintaining backward compatibility for existing clients that depend on older versions.
Why do APIs need versioning?
APIs evolve over time — new features, changed data structures, removed endpoints. Without versioning, any change can break existing client applications. Versioning lets you introduce changes in a new version while keeping the old version running for existing users.
Which API versioning strategy is best?
URL path versioning (/api/v1/users) is the most popular and recommended for public APIs. It's explicit, cacheable, easy to understand, and supported by all tools. Header versioning works well for internal APIs where you want cleaner URLs.
What is a breaking change in an API?
A breaking change is any modification that causes existing client code to fail. Examples: removing a field, renaming a field, changing data types, removing an endpoint, changing authentication method, or making a previously optional parameter required.
What is backward compatibility?
Backward compatibility means new API versions continue to support old client requests. Adding new optional fields is backward-compatible (old clients ignore them). Removing required fields is not backward-compatible (old clients break).
What is API deprecation?
Deprecation is the process of phasing out an old API version. It includes: announcing the deprecation, providing a migration guide, setting a sunset date, sending deprecation headers in responses, and eventually removing the old version after the migration period.
Should internal APIs be versioned?
It depends. If all consumers are controlled by your team and you can update them simultaneously, versioning may be unnecessary. But for APIs with multiple consumer teams, versioning prevents coordination nightmares and allows independent deployment.
How long should old API versions be supported?
Industry standard is 6-12 months after deprecation announcement. Critical enterprise APIs may support old versions for 2+ years. Monitor usage — when traffic to the old version drops below a threshold (e.g., 1%), it's safe to remove.
Can an API have multiple versions running simultaneously?
Yes, this is standard practice. Most large APIs run 2-3 versions simultaneously: the current stable version, the previous version (in deprecation), and optionally a beta/preview version. Each version is a separate code path or deployment.
URL versioning vs Header versioning — which is better?
URL versioning (/api/v2/) is more explicit, cacheable, and tool-friendly — best for public APIs. Header versioning (API-Version: 2) keeps URLs clean and is more RESTful in theory — better for internal APIs. Most developers and API platforms recommend URL versioning.

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.