Claude Code Plugins

Community-maintained marketplace

Feedback

api-versioning-strategy

@aj-geddes/useful-ai-prompts
4
0

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.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name api-versioning-strategy
description 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.

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

// 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)

// 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

// 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)

// ✅ 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)

// ❌ 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

// 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

// 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

// 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

// 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

# 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:

{
  "data": [...],
  "meta": {
    "total": 10,
    "page": 1,
    "perPage": 20
  }
}

Migration:

// 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:

// Before
const created = new Date(user.created);

// After
const created = new Date(user.created * 1000);

3. Error Format

v1:

{ "error": "User not found" }

v2:

{
  "error": {
    "code": "USER_NOT_FOUND",
    "message": "User not found",
    "details": {}
  }
}

New Features in v2

Pagination

// v2 supports cursor-based pagination
GET /api/v2/users?cursor=eyJpZCI6MTIzfQ&limit=20

Field Selection

// v2 supports field filtering
GET /api/v2/users?fields=id,name,email

Batch Operations

// v2 supports batch requests
POST /api/v2/batch
{
  "requests": [
    { "method": "GET", "path": "/users/1" },
    { "method": "GET", "path": "/users/2" }
  ]
}

Code Examples

JavaScript/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

# 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

// 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

// 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

// 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

// 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

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

// 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