- Add 100 blog posts covering AI, development, and tech topics - Add .env.example for environment configuration - Add accessibility and lighthouse audit scripts - Remove obsolete SEO reports and temporary files - Remove dev-dist build artifacts and backup files - Remove unused portrait images (moved/consolidated elsewhere) - Update contact form and component improvements Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
564 lines
17 KiB
Markdown
564 lines
17 KiB
Markdown
# 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
|
||
|
||
```typescript
|
||
// 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
|
||
|
||
```typescript
|
||
// 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
|
||
|
||
```typescript
|
||
// 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
|
||
|
||
```typescript
|
||
// 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
|
||
|
||
```typescript
|
||
// 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
|
||
|
||
```typescript
|
||
// 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
|
||
|
||
- [Stripe API Versioning](https://stripe.com/docs/api/versioning)
|
||
- [Microsoft REST API Guidelines](https://github.com/microsoft/api-guidelines/blob/vNext/Guidelines.md)
|
||
- [API Versioning Best Practices](https://www.freecodecamp.org/news/how-to-version-a-rest-api/)
|
||
- [Semantic Versioning](https://semver.org/)
|