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
-
User.id: number → string
- Affected: UserCard, UserList, UserProfile
- Action: Update type definitions
-
User.name → User.firstName + User.lastName
- Affected: UserCard, UserForm
- Action: Update display logic
Migration Plan
- Generate V2 types
- Create adapter functions
- Update components gradually
- Switch API client to V2
- 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
-
OpenAPI Contract
-
Type Generation
-
Error Contract