Claude Code for API Versioning: Strategies, Breaking Changes, and SDK Maintenance — Claude Skills 360 Blog
Blog / Development / Claude Code for API Versioning: Strategies, Breaking Changes, and SDK Maintenance
Development

Claude Code for API Versioning: Strategies, Breaking Changes, and SDK Maintenance

Published: August 29, 2026
Read time: 8 min read
By: Claude Skills 360

API versioning is one of those topics developers avoid until a breaking change forces the issue. Good versioning strategy lets you evolve your API without breaking existing integrations — and Claude Code helps implement it correctly: version routing, deprecation headers, backward-compatible expansion, and the patterns that delay needing major version bumps.

This guide covers API versioning with Claude Code: versioning strategies, backward-compatible evolution, deprecation workflows, and multi-version maintenance.

Versioning Strategy Choice

Should I version my API via URL (/v1/users) or via Accept headers?
What are the tradeoffs?

URL versioning (/v1/users, /v2/users):

  • Easy to test in browser/curl
  • Cache-friendly (different URL = different cached resource)
  • Clear from logs what version clients use
  • Con: “ugly” URLs; technically violates REST (same resource, different URL)

Header versioning (Accept: application/vnd.myapi.v2+json):

  • Semantically cleaner REST
  • Same URL for same resource
  • Con: harder to test/debug; caching requires Vary: Accept header

Recommendation: URL versioning for public APIs or APIs with external integrations. Header versioning when you control all clients.

URL Versioning Implementation

Implement URL versioning for a Node.js Express API.
All routes should work under /v1. New /v2 routes can
override specific endpoints.
// src/versioning/router.ts
import { Router, Request, Response, NextFunction } from 'express';

export function createVersionedRouter() {
  const router = Router();

  // Mount v1 routes
  const v1Router = require('./v1').default;
  router.use('/v1', v1Router);

  // Mount v2 routes (inherits unmodified v1 routes)
  const v2Router = createV2Router(v1Router);
  router.use('/v2', v2Router);

  return router;
}

function createV2Router(v1Router: Router): Router {
  const v2Router = Router();

  // v2 overrides specific endpoints
  const v2UsersRouter = require('./v2/users').default;
  v2Router.use('/users', v2UsersRouter);

  // All other routes fall back to v1
  v2Router.use('/', v1Router);

  return v2Router;
}
// src/versioning/v1/users.ts
import { Router } from 'express';

const router = Router();

router.get('/', async (req, res) => {
  const users = await db.users.findAll();
  // v1 response format: array directly
  res.json(users.map(u => ({
    id: u.id,
    name: u.name,
    email: u.email,
  })));
});

export default router;
// src/versioning/v2/users.ts — Breaking change: wrapped response + pagination
import { Router } from 'express';

const router = Router();

router.get('/', async (req, res) => {
  const page = parseInt(req.query.page as string) || 1;
  const users = await db.users.findPaginated({ page, perPage: 20 });
  
  // v2 response format: wrapped with metadata 
  res.json({
    data: users.items.map(u => ({
      id: u.id,
      fullName: u.name,        // Changed: name → fullName
      email: u.email,
      createdAt: u.createdAt,  // Added: new field
    })),
    pagination: {
      page: users.page,
      perPage: users.perPage,
      total: users.total,
    },
  });
});

export default router;

Deprecation Headers and Sunset

Implement deprecation warnings when clients call old API versions.
Show them when it's being removed and where to find the replacement.
// src/middleware/deprecation.ts
import { Request, Response, NextFunction } from 'express';

interface DeprecationConfig {
  sunsetDate: string; // ISO 8601
  deprecatedSince: string;
  link: string; // Migration guide URL
}

const versionDeprecations: Record<string, DeprecationConfig> = {
  'v1': {
    sunsetDate: '2026-12-31T23:59:59Z',
    deprecatedSince: '2026-06-01',
    link: 'https://docs.example.com/api/migration/v1-to-v2',
  },
};

export function deprecationMiddleware(version: string) {
  return (req: Request, res: Response, next: NextFunction) => {
    const config = versionDeprecations[version];
    if (!config) return next();

    // RFC 8594 Deprecation header
    res.setHeader('Deprecation', `date="${config.deprecatedSince}"`);
    res.setHeader('Sunset', config.sunsetDate);
    res.setHeader('Link', `<${config.link}>; rel="deprecation", <${config.link}>; rel="successor-version"`);

    // Log deprecated API usage for tracking
    console.warn(`Deprecated API call: ${version} ${req.method} ${req.path} - Client: ${req.headers['user-agent']}`);

    next();
  };
}

// Apply to v1 routes:
router.use('/v1', deprecationMiddleware('v1'), v1Router);

Backward-Compatible Evolution

I need to rename the 'fullName' field to separate 'firstName'/'lastName'.
How do I do this without a major version bump?
// Backward-compatible field expansion: add new, keep old
// v1 → v2-compatible approach (no major version needed)

// Step 1: Accept both old and new format in input
interface CreateUserInput {
  // New fields
  firstName?: string;
  lastName?: string;
  // Old field — still accepted, mapped to new fields
  fullName?: string; // @deprecated — use firstName + lastName
}

function normalizeUserInput(raw: CreateUserInput) {
  if (raw.fullName && !raw.firstName && !raw.lastName) {
    const parts = raw.fullName.split(' ');
    return {
      firstName: parts[0],
      lastName: parts.slice(1).join(' ') || '',
    };
  }
  return { firstName: raw.firstName ?? '', lastName: raw.lastName ?? '' };
}

// Step 2: Return both in output during transition period
interface UserResponse {
  id: string;
  firstName: string;
  lastName: string;
  fullName: string; // Computed for backward compat — remove in v3
  email: string;
}

function serializeUser(user: User): UserResponse {
  return {
    id: user.id,
    firstName: user.firstName,
    lastName: user.lastName,
    fullName: `${user.firstName} ${user.lastName}`, // Keep for backward compat
    email: user.email,
  };
}

Breaking Change Detection in CI

# .github/workflows/api-breaking-changes.yml
name: Detect API Breaking Changes

on:
  pull_request:
    paths:
      - 'src/routes/**'
      - 'api/openapi.yaml'

jobs:
  detect-breaking-changes:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      # Compare OpenAPI spec against main branch
      - name: Install oasdiff
        run: npm install -g @oasdiff/oasdiff

      - name: Check for breaking changes
        run: |
          git show origin/main:api/openapi.yaml > /tmp/api-main.yaml
          
          # oasdiff outputs breaking changes and exits 1 if any found
          oasdiff breaking /tmp/api-main.yaml api/openapi.yaml \
            --fail-on ERR 2>&1 | tee breaking-changes.txt
          
          if [ -s breaking-changes.txt ]; then
            echo "## ⚠️ Breaking API Changes Detected" >> $GITHUB_STEP_SUMMARY
            cat breaking-changes.txt >> $GITHUB_STEP_SUMMARY
            echo "" >> $GITHUB_STEP_SUMMARY
            echo "Breaking changes require a new major version (/v2). See: docs/api-versioning.md" >> $GITHUB_STEP_SUMMARY
          fi

SDK Version Management

// For SDKs that need to support multiple API versions:
// src/sdk/client.ts

type ApiVersion = 'v1' | 'v2';

interface ClientConfig {
  baseUrl: string;
  apiKey: string;
  version?: ApiVersion;
}

export class ApiClient {
  private version: ApiVersion;
  
  constructor(private config: ClientConfig) {
    this.version = config.version ?? 'v2'; // Default to latest
  }

  async listUsers(): Promise<User[]> {
    if (this.version === 'v1') {
      const data = await this.request<any[]>('/users');
      // Normalize v1 flat array to standard format
      return data.map(u => ({ ...u, firstName: u.name.split(' ')[0], lastName: u.name.split(' ').slice(1).join(' ') }));
    }
    const { data } = await this.request<{ data: User[]; pagination: any }>('/users');
    return data;
  }

  private async request<T>(path: string): Promise<T> {
    const response = await fetch(`${this.config.baseUrl}/${this.version}${path}`, {
      headers: { Authorization: `Bearer ${this.config.apiKey}` },
    });
    if (!response.ok) throw new Error(`API error: ${response.statusText}`);
    return response.json();
  }
}

For the OpenAPI spec contract testing that catches breaking changes before deployment, see the OpenAPI codegen guide. For API gateway patterns including rate limiting per API version, see the API gateway guide. The Claude Skills 360 bundle includes API design skill sets covering versioning strategies, deprecation workflows, and migration guides. Start with the free tier to try API versioning code generation.

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free