API Versioning - Quick Reference
When NOT to Use This Skill
- Database schema versioning - Use migration skills
- Feature flags - Use deployment skills
- Contract validation - Use
openapi-contract skill
Versioning Strategies
| Strategy |
Example |
Pros |
Cons |
| URL Path |
/api/v1/users |
Clear, cacheable |
URL changes |
| Query Param |
/api/users?version=1 |
Easy to implement |
Not RESTful |
| Header |
Accept: application/vnd.api.v1+json |
Clean URLs |
Less visible |
| Content Negotiation |
Accept: application/json; version=1 |
Flexible |
Complex |
Recommendation: URL Path Versioning
Most common, easiest to understand, best tooling support.
URL Path Versioning
Backend Implementation (NestJS)
// Version 1 controller
@Controller('api/v1/users')
export class UsersControllerV1 {
@Get()
findAll(): UserV1[] {
return this.usersService.findAllV1();
}
}
// Version 2 controller
@Controller('api/v2/users')
export class UsersControllerV2 {
@Get()
findAll(): UserV2[] {
return this.usersService.findAllV2();
}
}
// Or using NestJS built-in versioning
@Controller('users')
@Version('1')
export class UsersControllerV1 { ... }
@Controller('users')
@Version('2')
export class UsersControllerV2 { ... }
Backend Implementation (Spring Boot)
// Version 1 controller
@RestController
@RequestMapping("/api/v1/users")
public class UserControllerV1 {
@GetMapping
public List<UserDtoV1> getUsers() {
return userService.getUsersV1();
}
}
// Version 2 controller
@RestController
@RequestMapping("/api/v2/users")
public class UserControllerV2 {
@GetMapping
public List<UserDtoV2> getUsers() {
return userService.getUsersV2();
}
}
Frontend Configuration
// api/config.ts
const API_VERSION = process.env.NEXT_PUBLIC_API_VERSION || 'v1';
export const API_BASE_URL = `/api/${API_VERSION}`;
// api/client.ts
import createClient from 'openapi-fetch';
import type { paths } from './types';
const client = createClient<paths>({
baseUrl: API_BASE_URL,
});
// Usage
const users = await client.GET('/users'); // Calls /api/v1/users
Header Versioning
Backend Implementation
// NestJS with header versioning
app.enableVersioning({
type: VersioningType.HEADER,
header: 'X-API-Version',
});
@Controller('users')
@Version('1')
export class UsersControllerV1 { ... }
Frontend Implementation
const api = axios.create({
baseURL: '/api',
headers: {
'X-API-Version': '1',
},
});
// Or per-request
const response = await fetch('/api/users', {
headers: {
'X-API-Version': '2',
},
});
Version Coexistence
OpenAPI Spec per Version
# openapi-v1.yaml
openapi: 3.0.3
info:
title: My API
version: 1.0.0
servers:
- url: /api/v1
paths:
/users:
get:
responses:
200:
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/UserV1'
components:
schemas:
UserV1:
type: object
properties:
id:
type: integer
name:
type: string
email:
type: string
# openapi-v2.yaml
openapi: 3.0.3
info:
title: My API
version: 2.0.0
servers:
- url: /api/v2
paths:
/users:
get:
responses:
200:
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/UserV2'
components:
schemas:
UserV2:
type: object
properties:
id:
type: string # Changed to string!
firstName: # Split from name
type: string
lastName: # Split from name
type: string
email:
type: string
createdAt: # New field
type: string
format: date-time
Generate Types for Both
# Generate v1 types
npx openapi-typescript openapi-v1.yaml -o src/api/v1/types.ts
# Generate v2 types
npx openapi-typescript openapi-v2.yaml -o src/api/v2/types.ts
Frontend Version Support
// api/v1/client.ts
import createClient from 'openapi-fetch';
import type { paths } from './types';
export const clientV1 = createClient<paths>({
baseUrl: '/api/v1',
});
// api/v2/client.ts
import createClient from 'openapi-fetch';
import type { paths } from './types';
export const clientV2 = createClient<paths>({
baseUrl: '/api/v2',
});
// Use the appropriate version
import { clientV1 } from './api/v1/client';
import { clientV2 } from './api/v2/client';
// Migrating gradually
const users = await clientV2.GET('/users'); // Use v2 for users
const orders = await clientV1.GET('/orders'); // Still on v1 for orders
Migration Patterns
Adapter Pattern
// Adapt v1 response to v2 format
function adaptUserV1toV2(userV1: UserV1): UserV2 {
const [firstName, ...lastParts] = userV1.name.split(' ');
return {
id: String(userV1.id), // Convert number to string
firstName,
lastName: lastParts.join(' '),
email: userV1.email,
createdAt: new Date().toISOString(), // Default value
};
}
// Use during migration
async function getUsers(): Promise<UserV2[]> {
if (USE_V2_API) {
const { data } = await clientV2.GET('/users');
return data;
} else {
const { data } = await clientV1.GET('/users');
return data.map(adaptUserV1toV2);
}
}
Feature Flag Migration
// Gradual rollout with feature flag
async function getUsers(): Promise<User[]> {
const useV2 = await featureFlags.isEnabled('api-v2-users');
if (useV2) {
return fetchUsersV2();
}
return fetchUsersV1();
}
Backend Deprecation Headers
// NestJS - Add deprecation warning
@Controller('api/v1/users')
@Header('Deprecation', 'true')
@Header('Sunset', 'Sat, 01 Jan 2025 00:00:00 GMT')
@Header('Link', '</api/v2/users>; rel="successor-version"')
export class UsersControllerV1 { ... }
Frontend Deprecation Handling
axios.interceptors.response.use((response) => {
if (response.headers['deprecation'] === 'true') {
const sunset = response.headers['sunset'];
console.warn(
`API endpoint ${response.config.url} is deprecated. ` +
`Will be removed on ${sunset}`
);
// Track in analytics
analytics.track('deprecated_api_used', {
endpoint: response.config.url,
sunset,
});
}
return response;
});
Breaking vs Non-Breaking Changes
Non-Breaking (Safe)
| Change |
Example |
Action |
| Add optional field |
createdAt?: string |
No version bump |
| Add new endpoint |
GET /users/search |
No version bump |
| Add optional param |
?include=profile |
No version bump |
| Widen response type |
id: number | string |
No version bump |
Breaking (Requires New Version)
| Change |
Example |
Action |
| Remove field |
Remove name |
New version |
| Rename field |
name → fullName |
New version |
| Change type |
id: number → id: string |
New version |
| Change URL |
/users → /members |
New version |
| Add required field |
role: string (required) |
New version |
Validation Checklist
Per-Endpoint Check
| Check |
V1 |
V2 |
Frontend Uses |
Status |
| Base URL |
/api/v1 |
/api/v2 |
/api/v1 |
OK |
| User.id type |
number |
string |
number |
MISMATCH |
| User.name |
present |
split |
uses name |
MISMATCH |
| Response structure |
same |
same |
OK |
OK |
Migration Readiness
## Migration Readiness Report
### Endpoints Using V1
- GET /api/v1/users (10 components)
- POST /api/v1/users (3 components)
- GET /api/v1/orders (5 components)
### Breaking Changes in V2
1. User.id: number → string
- Affected: UserCard, UserList, UserProfile
- Action: Update type definitions
2. User.name → User.firstName + User.lastName
- Affected: UserCard, UserForm
- Action: Update display logic
### Migration Plan
1. [ ] Generate V2 types
2. [ ] Create adapter functions
3. [ ] Update components gradually
4. [ ] Switch API client to V2
5. [ ] Remove V1 code
Anti-Patterns
| Anti-Pattern |
Why It's Bad |
Correct Approach |
| Breaking changes without version |
Breaks clients |
Create new version |
| Mixing v1/v2 in same client |
Confusion |
Separate clients per version |
| No deprecation notice |
Surprise breakage |
Add sunset headers |
| Removing old version immediately |
Breaks clients |
Sunset period |
| Version in domain name |
Hard to manage |
Use URL path |
Quick Troubleshooting
| Issue |
Likely Cause |
Solution |
| Wrong response format |
Using wrong version |
Check API_VERSION config |
| 404 on new endpoint |
Still using old version |
Update base URL |
| Type errors |
Types don't match version |
Regenerate types |
| Deprecation warnings |
Using old version |
Plan migration |
| Mixed responses |
Inconsistent version use |
Audit all API calls |
Related Skills