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>
This commit is contained in:
@@ -0,0 +1,616 @@
|
||||
# API Design & Documentation
|
||||
|
||||
**Meta-Description:** REST API Design Best Practices. OpenAPI Specification, Dokumentation mit Swagger und Developer Experience optimieren.
|
||||
|
||||
**Keywords:** API Design, REST API, OpenAPI, Swagger, API Documentation, Developer Experience, API Best Practices
|
||||
|
||||
---
|
||||
|
||||
## Einführung
|
||||
|
||||
**Gutes API Design** ist entscheidend für Developer Experience. Mit **OpenAPI/Swagger** können APIs dokumentiert, getestet und generiert werden. Dieser Guide zeigt Best Practices für konsistente, intuitive APIs.
|
||||
|
||||
---
|
||||
|
||||
## API Design Overview
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ REST API DESIGN PRINCIPLES │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ URL Structure: │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ Resources (Nouns, plural): │ │
|
||||
│ │ ├── GET /users List users │ │
|
||||
│ │ ├── POST /users Create user │ │
|
||||
│ │ ├── GET /users/{id} Get user │ │
|
||||
│ │ ├── PUT /users/{id} Update user │ │
|
||||
│ │ ├── PATCH /users/{id} Partial update │ │
|
||||
│ │ └── DELETE /users/{id} Delete user │ │
|
||||
│ │ │ │
|
||||
│ │ Nested Resources: │ │
|
||||
│ │ ├── GET /users/{id}/projects │ │
|
||||
│ │ └── POST /users/{id}/projects │ │
|
||||
│ │ │ │
|
||||
│ │ Query Parameters: │ │
|
||||
│ │ ├── ?page=1&limit=20 Pagination │ │
|
||||
│ │ ├── ?sort=createdAt:desc Sorting │ │
|
||||
│ │ ├── ?filter[status]=active Filtering │ │
|
||||
│ │ └── ?fields=id,name,email Field selection │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ HTTP Status Codes: │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ 2xx Success: │ │
|
||||
│ │ ├── 200 OK Successful request │ │
|
||||
│ │ ├── 201 Created Resource created │ │
|
||||
│ │ └── 204 No Content Successful, no body │ │
|
||||
│ │ │ │
|
||||
│ │ 4xx Client Errors: │ │
|
||||
│ │ ├── 400 Bad Request Invalid input │ │
|
||||
│ │ ├── 401 Unauthorized Not authenticated │ │
|
||||
│ │ ├── 403 Forbidden Not authorized │ │
|
||||
│ │ ├── 404 Not Found Resource not found │ │
|
||||
│ │ └── 422 Unprocessable Validation failed │ │
|
||||
│ │ │ │
|
||||
│ │ 5xx Server Errors: │ │
|
||||
│ │ ├── 500 Internal Error Server error │ │
|
||||
│ │ └── 503 Service Unavailable Temporarily down │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## OpenAPI Specification
|
||||
|
||||
```yaml
|
||||
# openapi/api.yaml
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: My SaaS API
|
||||
description: API for managing projects and tasks
|
||||
version: 1.0.0
|
||||
contact:
|
||||
name: API Support
|
||||
email: api@example.com
|
||||
url: https://example.com/support
|
||||
license:
|
||||
name: MIT
|
||||
url: https://opensource.org/licenses/MIT
|
||||
|
||||
servers:
|
||||
- url: https://api.example.com/v1
|
||||
description: Production
|
||||
- url: https://api.staging.example.com/v1
|
||||
description: Staging
|
||||
- url: http://localhost:3000/api/v1
|
||||
description: Local development
|
||||
|
||||
tags:
|
||||
- name: Projects
|
||||
description: Project management endpoints
|
||||
- name: Tasks
|
||||
description: Task management endpoints
|
||||
- name: Users
|
||||
description: User management endpoints
|
||||
|
||||
paths:
|
||||
/projects:
|
||||
get:
|
||||
tags: [Projects]
|
||||
summary: List all projects
|
||||
description: Returns a paginated list of projects for the authenticated user
|
||||
operationId: listProjects
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/PageParam'
|
||||
- $ref: '#/components/parameters/LimitParam'
|
||||
- name: status
|
||||
in: query
|
||||
description: Filter by status
|
||||
schema:
|
||||
type: string
|
||||
enum: [active, archived, draft]
|
||||
- name: sort
|
||||
in: query
|
||||
description: Sort field and direction
|
||||
schema:
|
||||
type: string
|
||||
example: createdAt:desc
|
||||
responses:
|
||||
'200':
|
||||
description: Successful response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ProjectListResponse'
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalError'
|
||||
|
||||
post:
|
||||
tags: [Projects]
|
||||
summary: Create a project
|
||||
description: Creates a new project
|
||||
operationId: createProject
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/CreateProjectInput'
|
||||
responses:
|
||||
'201':
|
||||
description: Project created
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Project'
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'422':
|
||||
$ref: '#/components/responses/ValidationError'
|
||||
|
||||
/projects/{projectId}:
|
||||
get:
|
||||
tags: [Projects]
|
||||
summary: Get a project
|
||||
operationId: getProject
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/ProjectIdParam'
|
||||
responses:
|
||||
'200':
|
||||
description: Successful response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Project'
|
||||
'404':
|
||||
$ref: '#/components/responses/NotFound'
|
||||
|
||||
patch:
|
||||
tags: [Projects]
|
||||
summary: Update a project
|
||||
operationId: updateProject
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/ProjectIdParam'
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UpdateProjectInput'
|
||||
responses:
|
||||
'200':
|
||||
description: Project updated
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Project'
|
||||
|
||||
delete:
|
||||
tags: [Projects]
|
||||
summary: Delete a project
|
||||
operationId: deleteProject
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/ProjectIdParam'
|
||||
responses:
|
||||
'204':
|
||||
description: Project deleted
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
|
||||
parameters:
|
||||
ProjectIdParam:
|
||||
name: projectId
|
||||
in: path
|
||||
required: true
|
||||
description: Project ID
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
|
||||
PageParam:
|
||||
name: page
|
||||
in: query
|
||||
description: Page number
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
default: 1
|
||||
|
||||
LimitParam:
|
||||
name: limit
|
||||
in: query
|
||||
description: Items per page
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 100
|
||||
default: 20
|
||||
|
||||
schemas:
|
||||
Project:
|
||||
type: object
|
||||
required: [id, name, status, createdAt]
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
example: '123e4567-e89b-12d3-a456-426614174000'
|
||||
name:
|
||||
type: string
|
||||
minLength: 1
|
||||
maxLength: 100
|
||||
example: 'My Project'
|
||||
description:
|
||||
type: string
|
||||
maxLength: 1000
|
||||
status:
|
||||
type: string
|
||||
enum: [active, archived, draft]
|
||||
default: active
|
||||
createdAt:
|
||||
type: string
|
||||
format: date-time
|
||||
updatedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
|
||||
CreateProjectInput:
|
||||
type: object
|
||||
required: [name]
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
minLength: 1
|
||||
maxLength: 100
|
||||
description:
|
||||
type: string
|
||||
maxLength: 1000
|
||||
|
||||
UpdateProjectInput:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
minLength: 1
|
||||
maxLength: 100
|
||||
description:
|
||||
type: string
|
||||
maxLength: 1000
|
||||
status:
|
||||
type: string
|
||||
enum: [active, archived, draft]
|
||||
|
||||
ProjectListResponse:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Project'
|
||||
pagination:
|
||||
$ref: '#/components/schemas/Pagination'
|
||||
|
||||
Pagination:
|
||||
type: object
|
||||
properties:
|
||||
page:
|
||||
type: integer
|
||||
limit:
|
||||
type: integer
|
||||
total:
|
||||
type: integer
|
||||
totalPages:
|
||||
type: integer
|
||||
|
||||
Error:
|
||||
type: object
|
||||
properties:
|
||||
code:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
details:
|
||||
type: object
|
||||
|
||||
responses:
|
||||
BadRequest:
|
||||
description: Bad request
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
|
||||
Unauthorized:
|
||||
description: Unauthorized
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
|
||||
NotFound:
|
||||
description: Resource not found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
|
||||
ValidationError:
|
||||
description: Validation error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Error'
|
||||
- type: object
|
||||
properties:
|
||||
errors:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
field:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
|
||||
InternalError:
|
||||
description: Internal server error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Type Generation from OpenAPI
|
||||
|
||||
```typescript
|
||||
// scripts/generate-types.ts
|
||||
import { generateApi } from 'swagger-typescript-api';
|
||||
import path from 'path';
|
||||
|
||||
generateApi({
|
||||
name: 'api-client.ts',
|
||||
output: path.resolve(__dirname, '../lib/api'),
|
||||
input: path.resolve(__dirname, '../openapi/api.yaml'),
|
||||
httpClientType: 'fetch',
|
||||
generateClient: true,
|
||||
generateRouteTypes: true,
|
||||
generateResponses: true,
|
||||
extractRequestParams: true,
|
||||
extractRequestBody: true,
|
||||
unwrapResponseData: true,
|
||||
prettier: {
|
||||
printWidth: 100,
|
||||
singleQuote: true
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
```typescript
|
||||
// lib/api/client.ts - Generated API Client Usage
|
||||
import { Api } from './api-client';
|
||||
|
||||
const api = new Api({
|
||||
baseUrl: process.env.NEXT_PUBLIC_API_URL,
|
||||
securityWorker: async () => {
|
||||
const token = await getAccessToken();
|
||||
return token ? { headers: { Authorization: `Bearer ${token}` } } : {};
|
||||
}
|
||||
});
|
||||
|
||||
// Type-safe API calls
|
||||
export async function getProjects(params?: {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
status?: 'active' | 'archived' | 'draft';
|
||||
}) {
|
||||
const response = await api.projects.listProjects(params);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function createProject(input: {
|
||||
name: string;
|
||||
description?: string;
|
||||
}) {
|
||||
const response = await api.projects.createProject(input);
|
||||
return response.data;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Response Format
|
||||
|
||||
```typescript
|
||||
// lib/api/response.ts
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
interface ApiResponse<T> {
|
||||
data?: T;
|
||||
error?: {
|
||||
code: string;
|
||||
message: string;
|
||||
details?: unknown;
|
||||
};
|
||||
pagination?: {
|
||||
page: number;
|
||||
limit: number;
|
||||
total: number;
|
||||
totalPages: number;
|
||||
};
|
||||
}
|
||||
|
||||
export function success<T>(
|
||||
data: T,
|
||||
status: number = 200
|
||||
): NextResponse<ApiResponse<T>> {
|
||||
return NextResponse.json({ data }, { status });
|
||||
}
|
||||
|
||||
export function paginated<T>(
|
||||
data: T[],
|
||||
pagination: { page: number; limit: number; total: number }
|
||||
): NextResponse<ApiResponse<T[]>> {
|
||||
return NextResponse.json({
|
||||
data,
|
||||
pagination: {
|
||||
...pagination,
|
||||
totalPages: Math.ceil(pagination.total / pagination.limit)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function created<T>(data: T): NextResponse<ApiResponse<T>> {
|
||||
return success(data, 201);
|
||||
}
|
||||
|
||||
export function noContent(): NextResponse {
|
||||
return new NextResponse(null, { status: 204 });
|
||||
}
|
||||
|
||||
export function error(
|
||||
code: string,
|
||||
message: string,
|
||||
status: number = 400,
|
||||
details?: unknown
|
||||
): NextResponse<ApiResponse<never>> {
|
||||
return NextResponse.json(
|
||||
{ error: { code, message, details } },
|
||||
{ status }
|
||||
);
|
||||
}
|
||||
|
||||
export function badRequest(message: string, details?: unknown) {
|
||||
return error('BAD_REQUEST', message, 400, details);
|
||||
}
|
||||
|
||||
export function unauthorized(message: string = 'Unauthorized') {
|
||||
return error('UNAUTHORIZED', message, 401);
|
||||
}
|
||||
|
||||
export function forbidden(message: string = 'Forbidden') {
|
||||
return error('FORBIDDEN', message, 403);
|
||||
}
|
||||
|
||||
export function notFound(resource: string = 'Resource') {
|
||||
return error('NOT_FOUND', `${resource} not found`, 404);
|
||||
}
|
||||
|
||||
export function validationError(errors: Array<{ field: string; message: string }>) {
|
||||
return error('VALIDATION_ERROR', 'Validation failed', 422, { errors });
|
||||
}
|
||||
|
||||
export function internalError(message: string = 'Internal server error') {
|
||||
return error('INTERNAL_ERROR', message, 500);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Interactive Documentation
|
||||
|
||||
```tsx
|
||||
// app/docs/api/page.tsx
|
||||
import SwaggerUI from 'swagger-ui-react';
|
||||
import 'swagger-ui-react/swagger-ui.css';
|
||||
|
||||
export default function ApiDocsPage() {
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<SwaggerUI
|
||||
url="/openapi/api.yaml"
|
||||
docExpansion="list"
|
||||
defaultModelsExpandDepth={-1}
|
||||
displayRequestDuration={true}
|
||||
filter={true}
|
||||
showExtensions={true}
|
||||
tryItOutEnabled={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Alternative: Custom docs with syntax highlighting
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
||||
|
||||
function CodeExample({ code, language }: { code: string; language: string }) {
|
||||
return (
|
||||
<SyntaxHighlighter
|
||||
language={language}
|
||||
style={vscDarkPlus}
|
||||
customStyle={{ borderRadius: '8px' }}
|
||||
>
|
||||
{code}
|
||||
</SyntaxHighlighter>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Design Checklist
|
||||
|
||||
| Aspect | Best Practice |
|
||||
|--------|--------------|
|
||||
| **URLs** | Use nouns, plural, lowercase |
|
||||
| **HTTP Methods** | Match CRUD operations |
|
||||
| **Status Codes** | Use appropriate codes |
|
||||
| **Errors** | Consistent format with codes |
|
||||
| **Pagination** | Always paginate lists |
|
||||
| **Versioning** | Use URL versioning (/v1/) |
|
||||
| **Documentation** | OpenAPI specification |
|
||||
| **Authentication** | Bearer tokens, API keys |
|
||||
|
||||
---
|
||||
|
||||
## Fazit
|
||||
|
||||
Gutes API Design erfordert:
|
||||
|
||||
1. **Konsistenz**: Einheitliche Struktur und Namenskonventionen
|
||||
2. **OpenAPI**: Maschinenlesbare Spezifikation
|
||||
3. **Dokumentation**: Interaktiv und aktuell
|
||||
4. **Type Safety**: Generierte Typen aus Spec
|
||||
|
||||
Developer Experience beginnt mit gutem API Design.
|
||||
|
||||
---
|
||||
|
||||
## Bildprompts
|
||||
|
||||
1. "API documentation page with Swagger UI, interactive endpoints"
|
||||
2. "REST API resource diagram, CRUD operations visualization"
|
||||
3. "OpenAPI specification editor, YAML code with validation"
|
||||
|
||||
---
|
||||
|
||||
## Quellen
|
||||
|
||||
- [OpenAPI Specification](https://spec.openapis.org/oas/latest.html)
|
||||
- [REST API Design Guide](https://restfulapi.net/)
|
||||
- [Google API Design Guide](https://cloud.google.com/apis/design)
|
||||
- [Microsoft REST API Guidelines](https://github.com/microsoft/api-guidelines)
|
||||
Reference in New Issue
Block a user