Why APIs Need Versioning
APIs evolve over time. New features are added, data models change, and old patterns are replaced. Without versioning, any change could break existing clients. API versioning allows backward-incompatible changes to coexist with older versions, giving clients time to migrate.
Breaking vs. Non-Breaking Changes
Non-breaking changes (safe to deploy without a new version):
- Adding new optional fields to responses
- Adding new endpoints
- Adding optional query parameters
- Relaxing validation rules
- Fixing bugs in error messages
Breaking changes (require a new version):
- Removing or renaming a field
- Changing a field’s data type
- Making an optional field required
- Changing the URL structure
- Modifying authentication mechanisms
- Changing error response format
Versioning Strategies
URL Path Versioning
The most common approach — the version is part of the URL:
GET /api/v1/users
GET /api/v2/users
Pros: Easy to see, easy to cache, easy to route Cons: URL changes between versions, breaks REST purist principles
Header Versioning
Version specified in a custom header or Accept header:
GET /api/users
Accept: application/vnd.myapi.v2+json
Or custom header:
GET /api/users
X-API-Version: 2
Pros: Clean URLs, same resource different representations Cons: Hidden from URL, harder to debug and test in browser
Query Parameter Versioning
Version as a query parameter:
GET /api/users?version=2
Pros: Easy to switch, visible in URL Cons: Can be confused with resource filtering, caching complications
Comparison
| Aspect | URL Path | Header | Query Param |
|---|---|---|---|
| Visibility | High | Low | Medium |
| Caching | Easy | Complex | Complex |
| API gateway routing | Easy | Moderate | Moderate |
| Browser testing | Easy | Hard | Easy |
| REST compliance | Low | High | Medium |
| Industry adoption | Most common | Medium | Less common |
Testing API Versioning
Backward Compatibility Tests
For each new version, verify that existing clients still work:
- Response structure: v1 clients should still receive v1 response format
- Field presence: No fields removed from v1 responses
- Data types: No type changes in v1 responses
- Error formats: v1 error responses unchanged
- Authentication: v1 auth mechanisms still work
Version-Specific Tests
| Test | v1 | v2 |
|---|---|---|
| GET /users response | {name: "Alice"} | {firstName: "Alice", lastName: "..."} |
| Pagination | ?page=1&per_page=20 | ?cursor=abc&limit=20 |
| Error format | {error: "Not found"} | {error: {code: "NOT_FOUND", message: "..."}} |
Deprecation Testing
When a version is being deprecated, verify:
- Deprecation headers present:
Deprecation: true
Sunset: Sat, 01 Mar 2026 00:00:00 GMT
Link: </api/v2/docs>; rel="successor-version"
- Warning headers: Some APIs include
Warningheaders with deprecation info - Documentation links: Response or headers link to migration guide
- Sunset timeline: The deprecated version continues working until the sunset date
- After sunset: Returns 410 Gone or redirects to new version
Default Version Behavior
Test what happens when no version is specified:
- Does the API default to the latest version?
- Does the API default to v1 (safest for backward compatibility)?
- Does the API return an error requiring explicit version?
Cross-Version Data Consistency
Data created in v1 should be accessible in v2 and vice versa (unless intentionally separated):
1. POST /api/v1/users → Create user (v1 format)
2. GET /api/v2/users/{id} → Should return user in v2 format
3. PUT /api/v2/users/{id} → Update in v2 format
4. GET /api/v1/users/{id} → Should still return v1 format
Migration Testing
When clients migrate from v1 to v2:
- Verify all v1 functionality has v2 equivalents
- Test that v1 data is properly transformed in v2 responses
- Check that v1-specific features that are removed have documented alternatives
- Ensure authentication tokens work across both versions
Common Versioning Bugs
| Bug | How to Detect |
|---|---|
| v1 response includes v2-only fields | Compare v1 response schema to docs |
| v2 breaks v1 contract | Run v1 test suite against current deployment |
| No deprecation notice | Check response headers for deprecated versions |
| Data inconsistency between versions | Create in v1, read in v2, compare |
| Default version changes silently | Test requests without explicit version |
| Authentication mismatch | Use v1 tokens on v2 endpoints |
Hands-On Exercise
- Compare versions: If using an API with multiple versions (GitHub API v3 vs GraphQL v4), compare the same operation across versions.
- Test backward compatibility: Make requests to both old and new versions of an API endpoint and verify data consistency.
- Create a migration checklist: For a hypothetical v1 to v2 migration, list all changes and their test scenarios.
- Check deprecation: Find a deprecated API endpoint and verify it includes proper deprecation headers and sunset dates.
Key Takeaways
- API versioning enables backward-incompatible changes while keeping existing clients functional
- URL path versioning (/api/v2/) is the most common approach; header versioning is the most REST-compliant
- Breaking changes (removing fields, changing types) require new versions; non-breaking changes (adding optional fields) do not
- Test backward compatibility by running old version test suites against new deployments
- Verify deprecation communication: headers, sunset dates, documentation links, and migration guides