# API Versioning Strategy

> Implement API versioning strategies including URL versioning, header versioning, backward compatibility, deprecation strategies, and migration guides. Use when dealing with API versions, deprecating endpoints, or managing breaking changes.

- Skill: `majiayu000/api-versioning-strategy-3` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds add majiayu000/api-versioning-strategy-3`
- Raw SKILL.md: https://api.skillmd.com/api/skills/majiayu000/api-versioning-strategy-3/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: majiayu000 (https://skillmd.com/u/majiayu000)
- Updated: 2026-09-09
- Page: https://skillmd.com/skills/majiayu000/api-versioning-strategy-3

---


# API Versioning Strategy

## Overview

Comprehensive guide to API versioning approaches, deprecation strategies, backward compatibility techniques, and migration planning for REST APIs, GraphQL, and gRPC services.

## When to Use

- Designing new APIs with versioning from the start
- Adding breaking changes to existing APIs
- Deprecating old API versions
- Planning API migrations
- Ensuring backward compatibility
- Managing multiple API versions simultaneously
- Creating API documentation for different versions
- Implementing API version routing

## Instructions

### 1. **Versioning Approaches**

#### URL Path Versioning
```typescript
// express-router.ts
import express from 'express';

const app = express();

// Version 1
app.get('/api/v1/users', (req, res) => {
  res.json({
    users: [
      { id: 1, name: 'John Doe' }
    ]
  });
});

// Version 2 - Added email field
app.get('/api/v2/users', (req, res) => {
  res.json({
    users: [
      { id: 1, name: 'John Doe', email: 'john@example.com' }
    ]
  });
});

// Shared logic with version-specific transformations
app.get('/api/:version/users/:id', async (req, res) => {
  const user = await userService.findById(req.params.id);

  if (req.params.version === 'v1') {
    res.json({ id: user.id, name: user.name });
  } else if (req.params.version === 'v2') {
    res.json({ id: user.id, name: user.name, email: user.email });
  }
});
```

**Pros:** Simple, explicit, cache-friendly
**Cons:** URL pollution, harder to deprecate

#### Header Versioning (Content Negotiation)
```typescript
// header-versioning.ts
app.get('/api/users', (req, res) => {
  const version = req.headers['api-version'] || '1';

  switch (version) {
    case '1':
      return res.json(transformToV1(users));
    case '2':
      return res.json(transformToV2(users));
    default:
      return res.status(400).json({ error: 'Unsupported API version' });
  }
});

// Or using Accept header
app.get('/api/users', (req, res) => {
  const acceptHeader = req.headers['accept'];

  if (acceptHeader.includes('application/vnd.myapi.v2+json')) {
    return res.json(transformToV2(users));
  }

  // Default to v1
  return res.json(transformToV1(users));
});
```

**Pros:** Clean URLs, RESTful
**Cons:** Less visible, harder to test manually

#### Query Parameter Versioning
```typescript
// query-param-versioning.ts
app.get('/api/users', (req, res) => {
  const version = req.query.version || '1';

  if (version === '2') {
    return res.json(transformToV2(users));
  }

  return res.json(transformToV1(users));
});

// Usage: GET /api/users?version=2
```

**Pros:** Easy to implement, flexible
**Cons:** Not RESTful, can be overlooked

### 2. **Backward Compatibility Patterns**

#### Additive Changes (Non-Breaking)
```typescript
// ✅ Safe: Adding optional fields
interface UserV1 {
  id: string;
  name: string;
}

interface UserV2 extends UserV1 {
  email?: string;  // Optional field
  avatar?: string; // Optional field
}

// ✅ Safe: Adding new endpoints
app.post('/api/v1/users/:id/avatar', uploadAvatar);

// ✅ Safe: Accepting additional parameters
app.get('/api/v1/users', (req, res) => {
  const { page, limit, sortBy } = req.query; // New optional params
  const users = await userService.list({ page, limit, sortBy });
  res.json(users);
});
```

#### Breaking Changes (Require New Version)
```typescript
// ❌ Breaking: Removing fields
interface UserV1 {
  id: string;
  name: string;
  username: string;
}

interface UserV2 {
  id: string;
  name: string;
  // username removed - BREAKING!
}

// ❌ Breaking: Changing field types
interface UserV1 {
  id: string;
  created: string; // ISO string
}

interface UserV2 {
  id: string;
  created: number; // Unix timestamp - BREAKING!
}

// ❌ Breaking: Renaming fields
interface UserV1 {
  fullName: string;
}

interface UserV2 {
  name: string; // Renamed from fullName - BREAKING!
}

// ❌ Breaking: Changing response structure
// V1
{ users: [...], total: 10 }

// V2 - BREAKING!
{ data: [...], meta: { total: 10 } }
```

#### Handling Both Versions
```typescript
// version-adapter.ts
export class UserAdapter {
  toV1(user: User): UserV1Response {
    return {
      id: user.id,
      name: user.fullName,
      username: user.username,
      created: user.createdAt.toISOString()
    };
  }

  toV2(user: User): UserV2Response {
    return {
      id: user.id,
      name: user.fullName,
      email: user.email,
      profile: {
        avatar: user.avatarUrl,
        bio: user.bio
      },
      createdAt: user.createdAt.getTime()
    };
  }

  fromV1(data: UserV1Request): User {
    return {
      fullName: data.name,
      username: data.username,
      email: data.email || null
    };
  }

  fromV2(data: UserV2Request): User {
    return {
      fullName: data.name,
      username: data.username || generateUsername(data.email),
      email: data.email,
      avatarUrl: data.profile?.avatar,
      bio: data.profile?.bio
    };
  }
}

// Usage in controller
app.get('/api/:version/users/:id', async (req, res) => {
  const user = await userService.findById(req.params.id);
  const adapter = new UserAdapter();

  const response = req.params.version === 'v2'
    ? adapter.toV2(user)
    : adapter.toV1(user);

  res.json(response);
});
```

### 3. **Deprecation Strategy**

#### Deprecation Headers
```typescript
// deprecation-middleware.ts
export function deprecationWarning(version: string, sunsetDate: Date) {
  return (req, res, next) => {
    res.setHeader('Deprecation', 'true');
    res.setHeader('Sunset', sunsetDate.toUTCString());
    res.setHeader('Link', '</api/v2/docs>; rel="successor-version"');
    res.setHeader('X-API-Warn', `Version ${version} is deprecated. Please migrate to v2 by ${sunsetDate.toDateString()}`);
    next();
  };
}

// Apply to deprecated routes
app.use('/api/v1/*', deprecationWarning('v1', new Date('2024-12-31')));

app.get('/api/v1/users', (req, res) => {
  // Return v1 response with deprecation headers
  res.json(users);
});
```

#### Deprecation Response
```typescript
// Include deprecation info in response body
app.get('/api/v1/users', (req, res) => {
  res.json({
    _meta: {
      deprecated: true,
      sunsetDate: '2024-12-31',
      message: 'This API version is deprecated. Please migrate to v2.',
      migrationGuide: 'https://docs.example.com/migration-v1-to-v2'
    },
    users: [...]
  });
});
```

#### Gradual Deprecation Timeline
```typescript
// deprecation-stages.ts
enum DeprecationStage {
  SUPPORTED = 'supported',
  DEPRECATED = 'deprecated',
  SUNSET_ANNOUNCED = 'sunset_announced',
  READONLY = 'readonly',
  SHUTDOWN = 'shutdown'
}

const versionStatus = {
  'v1': {
    stage: DeprecationStage.READONLY,
    sunsetDate: new Date('2024-06-30'),
    message: 'Read-only mode. New writes are disabled.'
  },
  'v2': {
    stage: DeprecationStage.DEPRECATED,
    sunsetDate: new Date('2024-12-31'),
    message: 'Deprecated. Please migrate to v3.'
  },
  'v3': {
    stage: DeprecationStage.SUPPORTED,
    message: 'Current stable version.'
  }
};

// Middleware to enforce deprecation
app.use('/api/:version/*', (req, res, next) => {
  const status = versionStatus[req.params.version];

  if (!status) {
    return res.status(404).json({ error: 'API version not found' });
  }

  if (status.stage === DeprecationStage.SHUTDOWN) {
    return res.status(410).json({ error: 'API version no longer available' });
  }

  if (status.stage === DeprecationStage.READONLY &&
      ['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method)) {
    return res.status(403).json({
      error: 'API version is read-only',
      message: status.message
    });
  }

  // Add deprecation headers
  if (status.stage !== DeprecationStage.SUPPORTED) {
    res.setHeader('X-API-Deprecated', 'true');
    res.setHeader('X-API-Sunset', status.sunsetDate.toISOString());
  }

  next();
});
```

### 4. **Migration Guide Example**

```markdown
# API Migration Guide: v1 to v2

## Overview
Version 2 introduces breaking changes to improve consistency and add new features.

**Timeline:**
- 2024-01-01: v2 released
- 2024-06-01: v1 deprecated
- 2024-09-01: v1 read-only
- 2024-12-31: v1 shutdown

## Breaking Changes

### 1. Response Structure
**v1:**
```json
{
  "users": [...],
  "total": 10,
  "page": 1
}
```

**v2:**
```json
{
  "data": [...],
  "meta": {
    "total": 10,
    "page": 1,
    "perPage": 20
  }
}
```

**Migration:**
```javascript
// Before
const users = response.users;
const total = response.total;

// After
const users = response.data;
const total = response.meta.total;
```

### 2. Date Format
**v1:** ISO 8601 strings
**v2:** Unix timestamps

**Migration:**
```javascript
// Before
const created = new Date(user.created);

// After
const created = new Date(user.created * 1000);
```

### 3. Error Format
**v1:**
```json
{ "error": "User not found" }
```

**v2:**
```json
{
  "error": {
    "code": "USER_NOT_FOUND",
    "message": "User not found",
    "details": {}
  }
}
```

## New Features in v2

### Pagination
```javascript
// v2 supports cursor-based pagination
GET /api/v2/users?cursor=eyJpZCI6MTIzfQ&limit=20
```

### Field Selection
```javascript
// v2 supports field filtering
GET /api/v2/users?fields=id,name,email
```

### Batch Operations
```javascript
// v2 supports batch requests
POST /api/v2/batch
{
  "requests": [
    { "method": "GET", "path": "/users/1" },
    { "method": "GET", "path": "/users/2" }
  ]
}
```

## Code Examples

### JavaScript/TypeScript
```typescript
// v1 Client
class ApiClientV1 {
  async getUsers() {
    const response = await fetch('/api/v1/users');
    const data = await response.json();
    return data.users;
  }
}

// v2 Client
class ApiClientV2 {
  async getUsers() {
    const response = await fetch('/api/v2/users');
    const data = await response.json();
    return data.data; // Changed from .users to .data
  }
}
```

### Python
```python
# v1
response = requests.get(f"{base_url}/api/v1/users")
users = response.json()["users"]

# v2
response = requests.get(f"{base_url}/api/v2/users")
users = response.json()["data"]
```
```

### 5. **GraphQL Versioning**

```typescript
// GraphQL handles versioning differently - through schema evolution
// schema-v1.graphql
type User {
  id: ID!
  name: String!
  username: String!
}

// schema-v2.graphql (deprecated fields)
type User {
  id: ID!
  name: String!
  username: String! @deprecated(reason: "Use email instead")
  email: String!
  profile: Profile
}

type Profile {
  avatar: String
  bio: String
}

// Field deprecation in resolver
const resolvers = {
  User: {
    username: (user) => {
      console.warn('username field is deprecated, use email instead');
      return user.email;
    }
  }
};
```

### 6. **gRPC Versioning**

```protobuf
// v1/user.proto
syntax = "proto3";
package user.v1;

message User {
  string id = 1;
  string name = 2;
}

// v2/user.proto
syntax = "proto3";
package user.v2;

message User {
  string id = 1;
  string name = 2;
  string email = 3;
  Profile profile = 4;
}

message Profile {
  string avatar = 1;
  string bio = 2;
}

// Both versions can coexist
service UserServiceV1 {
  rpc GetUser (GetUserRequest) returns (user.v1.User);
}

service UserServiceV2 {
  rpc GetUser (GetUserRequest) returns (user.v2.User);
}
```

### 7. **Version Detection & Routing**

```typescript
// version-router.ts
import express from 'express';

export class VersionRouter {
  private versions = new Map<string, express.Router>();

  registerVersion(version: string, router: express.Router) {
    this.versions.set(version, router);
  }

  getMiddleware() {
    return (req, res, next) => {
      // Detect version from multiple sources
      const version = this.detectVersion(req);

      const router = this.versions.get(version);
      if (!router) {
        return res.status(400).json({
          error: 'Invalid API version',
          supportedVersions: Array.from(this.versions.keys())
        });
      }

      // Set version in request for logging
      req.apiVersion = version;

      // Use versioned router
      router(req, res, next);
    };
  }

  private detectVersion(req): string {
    // 1. Check URL path
    const pathMatch = req.path.match(/^\/api\/v(\d+)\//);
    if (pathMatch) return pathMatch[1];

    // 2. Check header
    if (req.headers['api-version']) {
      return req.headers['api-version'];
    }

    // 3. Check Accept header
    const acceptMatch = req.headers['accept']?.match(/application\/vnd\.myapi\.v(\d+)\+json/);
    if (acceptMatch) return acceptMatch[1];

    // 4. Check query parameter
    if (req.query.version) {
      return req.query.version;
    }

    // 5. Default version
    return '1';
  }
}

// Usage
const versionRouter = new VersionRouter();

versionRouter.registerVersion('1', v1Router);
versionRouter.registerVersion('2', v2Router);
versionRouter.registerVersion('3', v3Router);

app.use('/api', versionRouter.getMiddleware());
```

### 8. **Testing Multiple Versions**

```typescript
// api-version.test.ts
describe('API Versioning', () => {
  describe('v1', () => {
    it('should return user with v1 format', async () => {
      const response = await request(app)
        .get('/api/v1/users/1')
        .expect(200);

      expect(response.body).toHaveProperty('id');
      expect(response.body).toHaveProperty('name');
      expect(response.body).not.toHaveProperty('email');
    });
  });

  describe('v2', () => {
    it('should return user with v2 format', async () => {
      const response = await request(app)
        .get('/api/v2/users/1')
        .expect(200);

      expect(response.body).toHaveProperty('id');
      expect(response.body).toHaveProperty('name');
      expect(response.body).toHaveProperty('email');
      expect(response.body).toHaveProperty('profile');
    });

    it('should include deprecation headers for v1', async () => {
      const response = await request(app)
        .get('/api/v1/users/1');

      expect(response.headers['deprecation']).toBe('true');
      expect(response.headers['sunset']).toBeDefined();
    });
  });

  describe('version negotiation', () => {
    it('should use version from header', async () => {
      const response = await request(app)
        .get('/api/users/1')
        .set('API-Version', '2')
        .expect(200);

      expect(response.body).toHaveProperty('email');
    });

    it('should default to v1 if no version specified', async () => {
      const response = await request(app)
        .get('/api/users/1')
        .expect(200);

      expect(response.body).not.toHaveProperty('email');
    });
  });
});
```

## Best Practices

### ✅ DO
- Version from day one (even if v1)
- Document breaking vs non-breaking changes
- Provide clear migration guides with code examples
- Use semantic versioning principles
- Give 6-12 months deprecation notice
- Monitor usage of deprecated APIs
- Send deprecation warnings to API consumers
- Support at least 2 versions simultaneously
- Use adapters/transformers for version logic
- Test all supported versions
- Log which API version is being used
- Provide migration tooling when possible
- Be consistent with versioning approach

### ❌ DON'T
- Change API behavior without versioning
- Remove versions without notice
- Support too many versions (>3)
- Use different versioning strategies in same API
- Break APIs without incrementing version
- Forget to update documentation
- Deprecate too quickly (<6 months)
- Ignore feedback from API consumers
- Make every change a new version
- Use version numbers inconsistently

## Common Patterns

### Pattern 1: Version-Agnostic Core
```typescript
// Core logic remains version-agnostic
class UserService {
  async getUser(id: string): Promise<User> {
    return this.repository.findById(id);
  }
}

// Version-specific adapters
class UserV1Adapter {
  transform(user: User): UserV1 { /* ... */ }
}

class UserV2Adapter {
  transform(user: User): UserV2 { /* ... */ }
}
```

### Pattern 2: Feature Flags for Gradual Rollout
```typescript
app.get('/api/v2/users', async (req, res) => {
  const user = await userService.getUser(req.params.id);

  // Gradual rollout of new feature
  if (featureFlags.isEnabled('enhanced-profile', req.user.id)) {
    return res.json(transformWithEnhancedProfile(user));
  }

  return res.json(transformV2(user));
});
```

### Pattern 3: API Version Metrics
```typescript
// Track usage by version
app.use((req, res, next) => {
  const version = detectVersion(req);
  metrics.increment('api.requests', { version });
  next();
});
```

## Tools & Resources

- **OpenAPI/Swagger**: API documentation with version support
- **Postman**: API testing with version management
- **API Blueprint**: API design with versioning
- **Stoplight**: API design and documentation
- **Kong**: API gateway with version routing

