Add blog posts, cleanup unused files, update components
- 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>
This commit is contained in:
@@ -0,0 +1,563 @@
|
||||
# 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/)
|
||||
Reference in New Issue
Block a user