Menu
Back to Blog
1 min read
Backend

API Design Patterns für 2026

Moderne API Design Patterns. REST Best Practices, GraphQL vs tRPC, API Versioning und Error Handling Standards.

API DesignREST APIGraphQLtRPCAPI VersioningError Handling
API Design Patterns für 2026

API Design Patterns für 2026

Meta-Description: Moderne API Design Patterns. REST Best Practices, GraphQL vs tRPC, API Versioning und Error Handling Standards.

Keywords: API Design, REST API, GraphQL, tRPC, API Versioning, Error Handling, OpenAPI, API Security


Einführung

Gutes API Design entscheidet über Developer Experience und Wartbarkeit. 2026 stehen mehrere Paradigmen zur Verfügung: REST, GraphQL, tRPC – jedes mit eigenen Stärken für verschiedene Use Cases.


API Paradigmen im Vergleich

┌─────────────────────────────────────────────────────────────┐
│              API PARADIGMEN 2026                            │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  REST                                                       │
│  ├── Resource-orientiert                                   │
│  ├── HTTP Verben (GET, POST, PUT, DELETE)                  │
│  ├── Stateless                                             │
│  ├── Caching-freundlich                                    │
│  └── Best for: Public APIs, Microservices                  │
│                                                             │
│  GraphQL                                                    │
│  ├── Schema-first                                          │
│  ├── Single Endpoint                                       │
│  ├── Client-driven Queries                                 │
│  ├── Subscriptions (Real-time)                             │
│  └── Best for: Complex Data, Mobile Apps                   │
│                                                             │
│  tRPC                                                       │
│  ├── End-to-End Type Safety                                │
│  ├── No Code Generation                                    │
│  ├── Zod Validation                                        │
│  ├── React Query Integration                               │
│  └── Best for: TypeScript Monorepos                        │
│                                                             │
└─────────────────────────────────────────────────────────────┘

REST Best Practices

// 1. Resource Naming (Plural Nouns)
// ✅ Good
GET    /users
GET    /users/:id
POST   /users
PUT    /users/:id
DELETE /users/:id

// ❌ Bad
GET    /getUser
POST   /createUser
GET    /user-list

// 2. Nested Resources
GET    /users/:userId/posts
GET    /users/:userId/posts/:postId
POST   /users/:userId/posts

// 3. Query Parameters für Filtering/Sorting
GET    /posts?status=published&author=123&sort=-createdAt&limit=10&offset=20

// 4. HTTP Status Codes
// 200 OK - Success
// 201 Created - Resource created
// 204 No Content - Success, no body (DELETE)
// 400 Bad Request - Validation error
// 401 Unauthorized - Authentication required
// 403 Forbidden - Permission denied
// 404 Not Found - Resource not found
// 409 Conflict - Duplicate/Conflict
// 422 Unprocessable Entity - Semantic error
// 429 Too Many Requests - Rate limited
// 500 Internal Server Error - Server error
// Next.js App Router REST API
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';

const createUserSchema = z.object({
  email: z.string().email(),
  name: z.string().min(2)
});

// GET /api/users
export async function GET(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams;
  const limit = parseInt(searchParams.get('limit') || '10');
  const offset = parseInt(searchParams.get('offset') || '0');

  const users = await db.user.findMany({
    take: limit,
    skip: offset
  });

  const total = await db.user.count();

  return NextResponse.json({
    data: users,
    pagination: {
      total,
      limit,
      offset,
      hasMore: offset + limit < total
    }
  });
}

// POST /api/users
export async function POST(request: NextRequest) {
  try {
    const body = await request.json();
    const data = createUserSchema.parse(body);

    const user = await db.user.create({ data });

    return NextResponse.json(
      { data: user },
      { status: 201 }
    );
  } catch (error) {
    if (error instanceof z.ZodError) {
      return NextResponse.json(
        {
          error: 'Validation Error',
          details: error.errors
        },
        { status: 400 }
      );
    }
    throw error;
  }
}

Error Response Standard

// RFC 7807 Problem Details
interface ProblemDetails {
  type: string;        // URI Reference für Error Type
  title: string;       // Kurze Beschreibung
  status: number;      // HTTP Status Code
  detail?: string;     // Ausführliche Beschreibung
  instance?: string;   // URI der fehlerhaften Ressource
  [key: string]: unknown;  // Erweiterungen
}

// Beispiel Implementation
function createErrorResponse(
  status: number,
  title: string,
  detail?: string,
  extras?: Record<string, unknown>
): NextResponse {
  const body: ProblemDetails = {
    type: `https://api.example.com/errors/${status}`,
    title,
    status,
    detail,
    instance: `/api/request-id/${crypto.randomUUID()}`,
    timestamp: new Date().toISOString(),
    ...extras
  };

  return NextResponse.json(body, {
    status,
    headers: {
      'Content-Type': 'application/problem+json'
    }
  });
}

// Verwendung
if (!user) {
  return createErrorResponse(
    404,
    'User Not Found',
    `User with ID ${id} does not exist`
  );
}

// Validation Error mit Details
return createErrorResponse(
  400,
  'Validation Error',
  'The request body contains invalid data',
  {
    errors: [
      { field: 'email', message: 'Invalid email format' },
      { field: 'name', message: 'Name is required' }
    ]
  }
);

API Versioning Strategies

// 1. URL Versioning (Empfohlen für Breaking Changes)
// /api/v1/users
// /api/v2/users

// app/api/v1/users/route.ts
export async function GET() {
  // V1 Response Format
  return NextResponse.json({ users: [...] });
}

// app/api/v2/users/route.ts
export async function GET() {
  // V2 Response Format (z.B. neue Felder)
  return NextResponse.json({
    data: [...],
    meta: { version: 'v2' }
  });
}

// 2. Header Versioning
// Accept: application/vnd.api+json;version=2
export async function GET(request: NextRequest) {
  const accept = request.headers.get('Accept') || '';
  const version = accept.match(/version=(\d+)/)?.[1] || '1';

  if (version === '2') {
    return handleV2(request);
  }
  return handleV1(request);
}

// 3. Query Parameter (für Clients ohne Header-Kontrolle)
// /api/users?version=2
export async function GET(request: NextRequest) {
  const version = request.nextUrl.searchParams.get('version') || '1';
  // ...
}

Rate Limiting

import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';

const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(10, '10s'), // 10 Requests pro 10s
  analytics: true
});

// Middleware
export async function middleware(request: NextRequest) {
  const ip = request.ip ?? '127.0.0.1';
  const { success, limit, reset, remaining } = await ratelimit.limit(ip);

  if (!success) {
    return NextResponse.json(
      {
        error: 'Too Many Requests',
        retryAfter: Math.ceil((reset - Date.now()) / 1000)
      },
      {
        status: 429,
        headers: {
          'X-RateLimit-Limit': limit.toString(),
          'X-RateLimit-Remaining': remaining.toString(),
          'X-RateLimit-Reset': reset.toString(),
          'Retry-After': Math.ceil((reset - Date.now()) / 1000).toString()
        }
      }
    );
  }

  const response = NextResponse.next();
  response.headers.set('X-RateLimit-Limit', limit.toString());
  response.headers.set('X-RateLimit-Remaining', remaining.toString());
  return response;
}

Pagination Patterns

// 1. Offset Pagination (Einfach, aber langsam bei großen Datasets)
interface OffsetPagination {
  data: User[];
  pagination: {
    total: number;
    limit: number;
    offset: number;
    hasMore: boolean;
  };
}

// 2. Cursor Pagination (Empfohlen für große Datasets)
interface CursorPagination<T> {
  data: T[];
  pagination: {
    cursor: string | null;
    hasMore: boolean;
  };
}

async function getUsersWithCursor(cursor?: string, limit = 20) {
  const users = await db.user.findMany({
    take: limit + 1, // +1 um hasMore zu prüfen
    cursor: cursor ? { id: cursor } : undefined,
    orderBy: { createdAt: 'desc' }
  });

  const hasMore = users.length > limit;
  const data = hasMore ? users.slice(0, -1) : users;
  const nextCursor = hasMore ? data[data.length - 1].id : null;

  return {
    data,
    pagination: {
      cursor: nextCursor,
      hasMore
    }
  };
}

// 3. Keyset Pagination (Für sortierte Daten)
// /api/posts?after=2024-01-15T10:00:00Z&limit=20

OpenAPI Specification

// Mit Zod + zod-to-openapi
import { OpenAPIHono, createRoute, z } from '@hono/zod-openapi';

const app = new OpenAPIHono();

const UserSchema = z.object({
  id: z.string().uuid(),
  email: z.string().email(),
  name: z.string()
}).openapi('User');

const route = createRoute({
  method: 'get',
  path: '/users/{id}',
  request: {
    params: z.object({
      id: z.string().uuid()
    })
  },
  responses: {
    200: {
      content: {
        'application/json': {
          schema: UserSchema
        }
      },
      description: 'User found'
    },
    404: {
      content: {
        'application/json': {
          schema: z.object({
            error: z.string()
          })
        }
      },
      description: 'User not found'
    }
  }
});

app.openapi(route, (c) => {
  const { id } = c.req.valid('param');
  // ...
});

// OpenAPI Spec generieren
app.doc('/doc', {
  openapi: '3.1.0',
  info: {
    title: 'My API',
    version: '1.0.0'
  }
});

API Security Checklist

// 1. Authentication
// - JWT mit kurzer Expiration
// - Refresh Token Rotation
// - Secure Cookie Storage

// 2. Authorization
// - RBAC oder ABAC
// - Resource-level Permissions
// - Rate Limiting per User

// 3. Input Validation
// - Zod für alle Inputs
// - Sanitize User Input
// - File Upload Restrictions

// 4. Headers
const securityHeaders = {
  'Content-Security-Policy': "default-src 'self'",
  'X-Content-Type-Options': 'nosniff',
  'X-Frame-Options': 'DENY',
  'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
  'X-XSS-Protection': '1; mode=block'
};

// 5. CORS
const corsConfig = {
  origin: ['https://app.example.com'],
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  credentials: true,
  maxAge: 86400
};

Fazit

Gutes API Design 2026 bedeutet:

  1. Konsistenz: Einheitliche Naming, Responses, Errors
  2. Type Safety: Zod/OpenAPI für Contracts
  3. Performance: Pagination, Caching, Rate Limiting
  4. Security: Validation, Auth, Headers

Die Wahl zwischen REST/GraphQL/tRPC hängt vom Use Case ab.


Bildprompts

  1. "API endpoints connecting client and server, clean architecture diagram"
  2. "REST vs GraphQL vs tRPC comparison, three paths to data"
  3. "Security shield protecting API gateway, authentication visualization"

Quellen