api-versioning

v2026.09.24

API versioning strategies for frontend-backend compatibility. Covers URL versioning, header versioning, and migration patterns. USE WHEN: user asks about "API versioning", "v1 v2 API", "API migration", "breaking changes", "backward compatibility" DO NOT USE FOR: schema versioning - use database skills, feature flags - use deployment skills

GitHub
Install command
npx skhub add claude-dev-suite/api-versioning
Markdown
SKILL.md

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

StrategyExampleProsCons
URL Path/api/v1/usersClear, cacheableURL changes
Query Param/api/users?version=1Easy to implementNot RESTful
HeaderAccept: application/vnd.api.v1+jsonClean URLsLess visible
Content NegotiationAccept: application/json; version=1FlexibleComplex

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)

ChangeExampleAction
Add optional fieldcreatedAt?: stringNo version bump
Add new endpointGET /users/searchNo version bump
Add optional param?include=profileNo version bump
Widen response typeid: number | stringNo version bump

Breaking (Requires New Version)

ChangeExampleAction
Remove fieldRemove nameNew version
Rename fieldname → fullNameNew version
Change typeid: number → id: stringNew version
Change URL/users → /membersNew version
Add required fieldrole: string (required)New version

Validation Checklist

Per-Endpoint Check

CheckV1V2Frontend UsesStatus
Base URL/api/v1/api/v2/api/v1OK
User.id typenumberstringnumberMISMATCH
User.namepresentsplituses nameMISMATCH
Response structuresamesameOKOK

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-PatternWhy It's BadCorrect Approach
Breaking changes without versionBreaks clientsCreate new version
Mixing v1/v2 in same clientConfusionSeparate clients per version
No deprecation noticeSurprise breakageAdd sunset headers
Removing old version immediatelyBreaks clientsSunset period
Version in domain nameHard to manageUse URL path

Quick Troubleshooting

IssueLikely CauseSolution
Wrong response formatUsing wrong versionCheck API_VERSION config
404 on new endpointStill using old versionUpdate base URL
Type errorsTypes don't match versionRegenerate types
Deprecation warningsUsing old versionPlan migration
Mixed responsesInconsistent version useAudit all API calls

Related Skills

Discovery
Tags

No tags published for this skill.

Version
Latest version metadata

Version

v2026.09.24

Published

Sep 24, 2026

Category

Uncategorized

License

MIT

Source path

skills/integration-validation/api-versioning

Default branch

main

Latest commit

9496306

Tree SHA

fe4e2f1