Files
Portfolio/blog-posts/95-api-versioning.md
T
damjan_savicandClaude Opus 4.5 e1bbe5455d Initial commit: Portfolio Website
Vollständige Next.js 15 Portfolio-Website mit:
- Blog-System mit 100+ Artikeln
- Supabase-Integration
- Responsive Design mit Tailwind CSS
- TypeScript-Konfiguration
- Testing-Setup mit Vitest und Playwright

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 15:07:20 +01:00

17 KiB
Raw Blame History

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

Aspect Recommendation
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