Definition
APIs are not static - they evolve. You need to add features, fix design flaws, and improve performance. But here is the problem: other people are using your API. If you just change things, their applications break. API versioning solves this dilemma by letting you run multiple versions simultaneously, allowing old clients to keep working while new clients get new features.
Think of versioning as running parallel universes of your API. Version 1 lives in one universe with its original design. Version 2 exists in another universe with improvements and new capabilities. Clients choose which universe they want to live in, and you maintain both until enough clients have migrated that you can retire the old one.
There are several common strategies for versioning. URI versioning puts the version in the path (/api/v1/users), making it highly visible and simple. Header versioning uses HTTP headers (Accept: application/vnd.myapi.v2+json), keeping URLs cleaner but hiding the version. Query parameter versioning appends the version to requests (/api/users?version=2). Each approach has trade-offs, but the most important thing is picking one and being consistent. Your users should never have to guess which version they are using.
Example
Stripe API: Stripe uses date-based versioning where each API version is identified by a date like 2023-10-16. When you create an account, you are pinned to that day’s version. You can upgrade by changing a header, but your integrations will not break unexpectedly.
Twitter API: Twitter runs v1.1 and v2 simultaneously. Version 2 has a completely different structure, but v1.1 keeps working for clients who have not migrated yet. This gives developers time to update their apps.
Google Cloud APIs: Google uses version numbers in paths (/v1/projects, /v2/projects) and maintains detailed migration guides between versions. They announce sunset dates well in advance.
GitHub API: GitHub uses URL versioning for their REST API and a stable endpoint for GraphQL. They communicate breaking changes through their changelog and give developers months to prepare.
Analogy
The iPhone and iOS: Apple releases new iPhones and iOS versions, but old phones keep working. Apps built for iOS 15 still run on iOS 17 (mostly). Apple maintains backward compatibility while adding new features. API versioning follows the same principle - old integrations keep working even as the API improves.
The Restaurant Menu Evolution: A restaurant might revise its menu, adding new dishes and removing old ones. But regular customers are warned in advance: “We will stop serving the chicken parmesan in 3 months.” API deprecation works the same way - versions are announced for sunset with plenty of warning.
The Highway Lane Closure: When highway construction needs to close a lane, signs warn drivers miles in advance, and traffic shifts gradually. APIs do the same thing - they announce version sunsets, provide migration paths, and give users time to transition.
The USB Standards: USB 2.0, 3.0, 3.1, and 4.0 all coexist. Newer devices work with older cables (at reduced speeds). API versioning aims for similar compatibility - new versions add capabilities without breaking basic functionality.
Code Example
// 1. URI/Path versioning (most common)
GET /api/v1/users/123
GET /api/v2/users/123
// 2. Header versioning
GET /api/users/123
Accept: application/vnd.myapi.v2+json
API-Version: 2
// 3. Query parameter versioning
GET /api/users/123?version=2
// 4. Content negotiation
GET /api/users/123
Accept: application/vnd.myapi+json;version=2
// Version detection example
app.get('/api/:version/users/:id', (req, res) => {
const version = req.params.version;
if (version === 'v1') {
return v1Handler(req, res);
} else if (version === 'v2') {
return v2Handler(req, res);
}
});
Security Notes
CRITICAL: API versioning allows evolution while maintaining compatibility. Plan strategy early.
Versioning Strategies:
- URI versioning: /v1/, /v2/ in path (most common)
- Header versioning: Accept: application/vnd.api+json;version=2
- Query parameter: ?version=2 (less common)
- Subdomain: v2.api.example.com (complex with CORS)
- Content negotiation: Accept header for version
Backward Compatibility:
- Add fields: Safe; old clients ignore new fields
- Rename fields: Breaking; old clients fail
- Change types: Breaking; causes parsing errors
- Remove fields: Breaking; old clients fail
- Deprecate gradually: Announce before removing
Deprecation Policy:
- Announce early: Announce deprecation well in advance
- Support window: Define how long old versions are supported
- Migration guide: Provide guide for migrating
- Sunset date: Clear date when old version stops working
- Notifications: Notify clients of deprecation
Version Management:
- Single codebase: Maintain single codebase supporting multiple versions
- Branch per version: Maintain separate branch per major version
- Compatibility layer: Layer translating between versions
- Feature flags: Use flags to enable/disable features per version
- Testing: Test compatibility across versions