Menu
Nazad na Blog
2 min read
Backend

API Versioning Strategies

API Versioning implementieren. URL Versioning, Header Versioning und Breaking Changes ohne Downtime managen.

API VersioningSemantic VersioningBreaking ChangesBackward CompatibilityAPI EvolutionDeprecation
API Versioning Strategies

API Versioning Strategies

Meta-Description: API Versioning implementieren. URL Versioning, Header Versioning und Breaking Changes ohne Downtime managen.

Keywords: API Versioning, Semantic Versioning, Breaking Changes, Backward Compatibility, API Evolution, Deprecation


Einführung

API Versioning ermöglicht Evolution ohne Breaking Changes für bestehende Clients. Von URL-based bis Header-based – verschiedene Strategien haben unterschiedliche Trade-offs. Dieser Guide zeigt Best Practices für nachhaltige API-Entwicklung.


Versioning Strategies

┌─────────────────────────────────────────────────────────────┐
│              API VERSIONING STRATEGIES                       │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  1. URL PATH VERSIONING (Recommended):                     │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  /api/v1/users                                      │   │
│  │  /api/v2/users                                      │   │
│  │                                                     │   │
│  │  ✓ Explicit, easy to understand                    │   │
│  │  ✓ Easy to route and cache                         │   │
│  │  ✓ Can run versions in parallel                    │   │
│  │  ✗ URL changes for version bumps                   │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  2. HEADER VERSIONING:                                     │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  Accept: application/vnd.myapi.v2+json              │   │
│  │  X-API-Version: 2                                   │   │
│  │                                                     │   │
│  │  ✓ Clean URLs                                      │   │
│  │  ✓ Follows HTTP semantics                          │   │
│  │  ✗ Harder to test (need headers)                   │   │
│  │  ✗ Not visible in URL                              │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  3. QUERY PARAMETER:                                       │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  /api/users?version=2                               │   │
│  │                                                     │   │
│  │  ✓ Easy to implement                               │   │
│  │  ✗ Optional parameter = default version issues     │   │
│  │  ✗ Caching complications                           │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  4. DATE-BASED VERSIONING (Stripe-style):                  │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  Stripe-Version: 2024-01-15                         │   │
│  │                                                     │   │
│  │  ✓ Fine-grained control                            │   │
│  │  ✓ Clear timeline                                  │   │
│  │  ✗ Many versions to maintain                       │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  Semantic Versioning (for SDKs):                           │
│  ├── MAJOR.MINOR.PATCH (1.2.3)                            │
│  ├── MAJOR: Breaking changes                              │
│  ├── MINOR: New features, backward compatible             │
│  └── PATCH: Bug fixes                                     │
│                                                             │
└─────────────────────────────────────────────────────────────┘

URL Versioning Implementation

// app/api/v1/users/route.ts
import { NextResponse } from 'next/server';

export async function GET() {
  // V1 response format
  const users = await db.user.findMany({
    select: {
      id: true,
      name: true,
      email: true
    }
  });

  return NextResponse.json({
    users, // Array at root level (v1 format)
    count: users.length
  });
}

// app/api/v2/users/route.ts
export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const page = parseInt(searchParams.get('page') || '1');
  const limit = parseInt(searchParams.get('limit') || '20');

  const [users, total] = await Promise.all([
    db.user.findMany({
      skip: (page - 1) * limit,
      take: limit,
      select: {
        id: true,
        name: true,
        email: true,
        createdAt: true, // New field in v2
        avatar: true      // New field in v2
      }
    }),
    db.user.count()
  ]);

  // V2 response format (standardized)
  return NextResponse.json({
    data: users,
    pagination: {
      page,
      limit,
      total,
      totalPages: Math.ceil(total / limit)
    }
  });
}

Version Router

// lib/api/version-router.ts
import { NextRequest, NextResponse } from 'next/server';

interface VersionConfig {
  current: string;
  supported: string[];
  deprecated: string[];
  sunset: Record<string, Date>;
}

const versionConfig: VersionConfig = {
  current: 'v3',
  supported: ['v2', 'v3'],
  deprecated: ['v1'],
  sunset: {
    v1: new Date('2024-06-01')
  }
};

type VersionHandler = (req: NextRequest) => Promise<NextResponse>;

interface VersionedHandlers {
  v1?: VersionHandler;
  v2?: VersionHandler;
  v3?: VersionHandler;
}

export function versionedRoute(handlers: VersionedHandlers) {
  return async (req: NextRequest): Promise<NextResponse> => {
    // Extract version from URL
    const version = extractVersion(req.nextUrl.pathname);

    // Check if version is supported
    if (!versionConfig.supported.includes(version) &&
        !versionConfig.deprecated.includes(version)) {
      return NextResponse.json(
        { error: `API version ${version} is not supported` },
        { status: 400 }
      );
    }

    // Get handler for version
    const handler = handlers[version as keyof VersionedHandlers];
    if (!handler) {
      return NextResponse.json(
        { error: `No handler for version ${version}` },
        { status: 501 }
      );
    }

    // Execute handler
    const response = await handler(req);

    // Add deprecation headers if needed
    if (versionConfig.deprecated.includes(version)) {
      const sunsetDate = versionConfig.sunset[version];
      response.headers.set('Deprecation', 'true');
      response.headers.set('Sunset', sunsetDate.toUTCString());
      response.headers.set(
        'Link',
        `</api/${versionConfig.current}>; rel="successor-version"`
      );
    }

    // Add version header
    response.headers.set('X-API-Version', version);

    return response;
  };
}

function extractVersion(pathname: string): string {
  const match = pathname.match(/\/api\/(v\d+)\//);
  return match ? match[1] : versionConfig.current;
}

// Usage
export const GET = versionedRoute({
  v1: async (req) => {
    // V1 implementation
    return NextResponse.json({ format: 'v1' });
  },
  v2: async (req) => {
    // V2 implementation
    return NextResponse.json({ data: [], format: 'v2' });
  },
  v3: async (req) => {
    // V3 implementation
    return NextResponse.json({ data: [], meta: {}, format: 'v3' });
  }
});

Breaking Changes Management

// lib/api/breaking-changes.ts
interface BreakingChange {
  id: string;
  version: string;
  date: Date;
  description: string;
  migration: string;
  affectedEndpoints: string[];
}

export const breakingChanges: BreakingChange[] = [
  {
    id: 'bc-001',
    version: 'v2',
    date: new Date('2024-01-15'),
    description: 'Response format changed to use data wrapper',
    migration: `
      // Before (v1):
      const users = response.users;

      // After (v2):
      const users = response.data;
    `,
    affectedEndpoints: ['/users', '/projects', '/tasks']
  },
  {
    id: 'bc-002',
    version: 'v2',
    date: new Date('2024-01-15'),
    description: 'Pagination parameters renamed',
    migration: `
      // Before (v1):
      GET /users?offset=0&count=20

      // After (v2):
      GET /users?page=1&limit=20
    `,
    affectedEndpoints: ['/users', '/projects']
  },
  {
    id: 'bc-003',
    version: 'v3',
    date: new Date('2024-06-01'),
    description: 'Date format changed to ISO 8601',
    migration: `
      // Before (v2):
      { "created": "2024-01-15 10:30:00" }

      // After (v3):
      { "createdAt": "2024-01-15T10:30:00.000Z" }
    `,
    affectedEndpoints: ['*']
  }
];

// Helper to check compatibility
export function getBreakingChanges(
  fromVersion: string,
  toVersion: string
): BreakingChange[] {
  const fromNum = parseInt(fromVersion.replace('v', ''));
  const toNum = parseInt(toVersion.replace('v', ''));

  return breakingChanges.filter(change => {
    const changeNum = parseInt(change.version.replace('v', ''));
    return changeNum > fromNum && changeNum <= toNum;
  });
}

Deprecation Workflow

// lib/api/deprecation.ts
import { db } from '@/lib/db';

interface DeprecationNotice {
  endpoint: string;
  version: string;
  deprecatedAt: Date;
  sunsetAt: Date;
  replacement?: string;
  reason: string;
}

const deprecationNotices: DeprecationNotice[] = [
  {
    endpoint: '/api/v1/users',
    version: 'v1',
    deprecatedAt: new Date('2024-01-01'),
    sunsetAt: new Date('2024-06-01'),
    replacement: '/api/v2/users',
    reason: 'Migrating to standardized response format'
  }
];

// Track deprecated endpoint usage
export async function trackDeprecatedUsage(
  apiKeyId: string,
  endpoint: string,
  version: string
): Promise<void> {
  await db.deprecatedUsage.upsert({
    where: {
      apiKeyId_endpoint_version: { apiKeyId, endpoint, version }
    },
    update: {
      lastUsedAt: new Date(),
      usageCount: { increment: 1 }
    },
    create: {
      apiKeyId,
      endpoint,
      version,
      usageCount: 1,
      firstUsedAt: new Date(),
      lastUsedAt: new Date()
    }
  });
}

// Send deprecation warnings to users
export async function sendDeprecationWarnings(): Promise<void> {
  const activeDeprecations = deprecationNotices.filter(
    d => d.sunsetAt > new Date()
  );

  for (const notice of activeDeprecations) {
    // Find users still using deprecated endpoints
    const usages = await db.deprecatedUsage.findMany({
      where: {
        endpoint: notice.endpoint,
        version: notice.version,
        lastUsedAt: {
          gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) // Last 7 days
        }
      },
      include: {
        apiKey: { include: { user: true } }
      }
    });

    for (const usage of usages) {
      await sendEmail(usage.apiKey.user.email, 'api-deprecation-warning', {
        endpoint: notice.endpoint,
        currentVersion: notice.version,
        replacement: notice.replacement,
        sunsetDate: notice.sunsetAt,
        usageCount: usage.usageCount
      });
    }
  }
}

// Middleware to add deprecation headers
export function deprecationMiddleware(
  endpoint: string,
  version: string
): Record<string, string> {
  const notice = deprecationNotices.find(
    d => d.endpoint === endpoint && d.version === version
  );

  if (!notice) return {};

  return {
    'Deprecation': notice.deprecatedAt.toUTCString(),
    'Sunset': notice.sunsetAt.toUTCString(),
    'Link': notice.replacement
      ? `<${notice.replacement}>; rel="successor-version"`
      : ''
  };
}

Migration Guide Generator

// lib/api/migration-guide.ts
export function generateMigrationGuide(
  fromVersion: string,
  toVersion: string
): string {
  const changes = getBreakingChanges(fromVersion, toVersion);

  let guide = `# Migration Guide: ${fromVersion} to ${toVersion}\n\n`;
  guide += `This guide covers all breaking changes when upgrading from API ${fromVersion} to ${toVersion}.\n\n`;

  guide += `## Summary\n\n`;
  guide += `- ${changes.length} breaking changes\n`;
  guide += `- Affected endpoints: ${[...new Set(changes.flatMap(c => c.affectedEndpoints))].join(', ')}\n\n`;

  guide += `## Changes\n\n`;

  changes.forEach((change, index) => {
    guide += `### ${index + 1}. ${change.description}\n\n`;
    guide += `**Version:** ${change.version}\n`;
    guide += `**Date:** ${change.date.toISOString().split('T')[0]}\n`;
    guide += `**Affected Endpoints:** ${change.affectedEndpoints.join(', ')}\n\n`;
    guide += `#### Migration\n\n`;
    guide += '```typescript\n';
    guide += change.migration;
    guide += '\n```\n\n';
  });

  guide += `## Need Help?\n\n`;
  guide += `If you encounter issues during migration, please:\n`;
  guide += `- Check our [API documentation](/docs/api/${toVersion})\n`;
  guide += `- Contact support at api-support@example.com\n`;

  return guide;
}

Client SDK Versioning

// sdk/src/versioned-client.ts
type APIVersion = 'v1' | 'v2' | 'v3';

interface VersionedClientConfig {
  apiKey: string;
  version?: APIVersion;
  baseUrl?: string;
}

export class VersionedClient {
  private version: APIVersion;
  private baseUrl: string;

  constructor(config: VersionedClientConfig) {
    this.version = config.version || 'v3'; // Default to latest
    this.baseUrl = config.baseUrl || `https://api.example.com/${this.version}`;
  }

  // Response transformers for backward compatibility
  private transformResponse<T>(data: unknown): T {
    if (this.version === 'v1') {
      // Transform v3 response to v1 format
      return this.transformToV1(data) as T;
    }
    if (this.version === 'v2') {
      return this.transformToV2(data) as T;
    }
    return data as T;
  }

  private transformToV1(data: any): any {
    // Remove data wrapper, convert dates, etc.
    if (data.data) {
      return {
        ...data.data,
        count: data.pagination?.total
      };
    }
    return data;
  }

  private transformToV2(data: any): any {
    // Keep data wrapper but adjust format
    return data;
  }

  async getUsers(): Promise<User[]> {
    const response = await this.request('/users');
    return this.transformResponse(response);
  }
}

// Factory for version-specific clients
export function createClient(version: APIVersion, apiKey: string) {
  switch (version) {
    case 'v1':
      console.warn('API v1 is deprecated. Please upgrade to v3.');
      return new VersionedClient({ apiKey, version: 'v1' });
    case 'v2':
      return new VersionedClient({ apiKey, version: 'v2' });
    case 'v3':
    default:
      return new VersionedClient({ apiKey, version: 'v3' });
  }
}

Best Practices

AspectRecommendation
**Format**URL versioning (most common)
**Increment**Only for breaking changes
**Support**Min 12-18 months per version
**Communication**6+ months deprecation notice
**Headers**Deprecation & Sunset headers
**Docs**Version-specific documentation

Fazit

API Versioning erfordert:

  1. Strategie: URL-based ist am klarsten
  2. Kommunikation: Frühe Deprecation Notices
  3. Support: Alte Versionen lang genug pflegen
  4. Migration: Klare Guides und Tools

Gutes Versioning ermöglicht Evolution ohne Client-Breakage.


Bildprompts

  1. "API versioning timeline, version lifecycle visualization"
  2. "Breaking changes migration flowchart, before and after"
  3. "Deprecation warning email template, sunset date highlighted"

Quellen