Zum Hauptinhalt springen
Zurück zum Blog
API Architektur Skalierbarkeit

APIs skalierbar gestalten: Architekturleitfaden

Wie Sie APIs entwerfen, die mit Ihrem Produkt skalieren. Rate Limiting, Caching, Datenbankoptimierung, Versionierung und Load-Balancing-Strategien.

JM
Javier Manzano
28. Juni 2026

Eine API zu bauen, die für 100 Benutzer funktioniert, ist einfach. Eine API zu bauen, die für 100.000 oder 1 Million Benutzer funktioniert — ohne grundlegend umgeschrieben zu werden — erfordert Architekturentscheidungen, die von Anfang an getroffen werden müssen.

Dieser Leitfaden behandelt die wichtigsten Designprinzipien für skalierbare APIs.

Datenbankoptimierung: Der häufigste Bottleneck

Indizes konsequent nutzen

-- Schlechte Query: Full Table Scan
SELECT * FROM orders WHERE customer_id = 123 AND status = 'pending';

-- Index hinzufügen
CREATE INDEX idx_orders_customer_status ON orders(customer_id, status);

-- EXPLAIN ANALYZE prüfen
EXPLAIN ANALYZE SELECT * FROM orders WHERE customer_id = 123 AND status = 'pending';
-- Ziel: Index Scan statt Seq Scan

N+1-Problem vermeiden

// Schlecht: N+1 Queries (1 Query für Users + N Queries für Orders)
const users = await db.users.findMany();
for (const user of users) {
  user.orders = await db.orders.findMany({ where: { userId: user.id } });
}

// Gut: Eager Loading mit JOIN
const users = await db.users.findMany({
  include: {
    orders: {
      where: { status: 'active' },
      select: { id: true, total: true, createdAt: true }
    }
  }
});

Read Replicas für Lesequeries

// Trennung: Schreibzugriffe auf Primary, Lesezugriffe auf Replica
const primaryDb = new Pool({ connectionString: process.env.DATABASE_PRIMARY_URL });
const replicaDb = new Pool({ connectionString: process.env.DATABASE_REPLICA_URL });

// Lese-API-Endpunkte nutzen Replica
app.get('/api/products', async (req, res) => {
  const products = await replicaDb.query('SELECT * FROM products WHERE active = true');
  res.json(products.rows);
});

// Schreib-Endpunkte nutzen Primary
app.post('/api/orders', async (req, res) => {
  const order = await primaryDb.query('INSERT INTO orders ...', [...]);
  res.json(order.rows[0]);
});

Caching-Strategien

In-Memory-Cache mit Redis

import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);

async function getProduct(productId) {
  const cacheKey = `product:${productId}`;

  // Cache-Hit prüfen
  const cached = await redis.get(cacheKey);
  if (cached) return JSON.parse(cached);

  // Cache-Miss: Datenbank abfragen
  const product = await db.products.findUnique({ where: { id: productId } });

  // In Cache speichern (TTL: 1 Stunde)
  await redis.setex(cacheKey, 3600, JSON.stringify(product));

  return product;
}

// Cache invalidieren bei Updates
async function updateProduct(productId, data) {
  await db.products.update({ where: { id: productId }, data });
  await redis.del(`product:${productId}`); // Cache löschen
}

HTTP-Caching mit ETags

app.get('/api/products/:id', async (req, res) => {
  const product = await getProduct(req.params.id);

  // ETag aus Daten-Hash generieren
  const etag = crypto.createHash('md5').update(JSON.stringify(product)).digest('hex');

  // Prüfen ob Client aktuelles Datenstand hat
  if (req.headers['if-none-match'] === etag) {
    return res.status(304).end(); // Not Modified - keine Daten übertragen
  }

  res.set('ETag', etag);
  res.set('Cache-Control', 'private, max-age=60'); // 60 Sekunden Cache
  res.json(product);
});

Rate Limiting

import rateLimit from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';

// Standard Rate Limit: 100 Anfragen pro 15 Minuten
const standardLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 100,
  store: new RedisStore({ client: redis }), // Über mehrere Server-Instanzen verteilt
  message: { error: 'Rate limit exceeded', retryAfter: 900 },
  headers: true, // X-RateLimit-* Headers setzen
});

// Strikterer Limit für Auth-Endpunkte
const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 10,
  store: new RedisStore({ client: redis }),
});

app.use('/api/', standardLimiter);
app.use('/api/auth/', authLimiter);

// Per-User Rate Limiting (für authentifizierte APIs)
const userLimiter = rateLimit({
  windowMs: 60 * 1000, // 1 Minute
  max: 60, // 60 Anfragen/Minute pro Benutzer
  keyGenerator: (req) => req.user?.id || req.ip, // Pro User statt Pro IP
  store: new RedisStore({ client: redis }),
});

API-Versionierung

// Version in URL (empfohlen für öffentliche APIs)
app.use('/api/v1', v1Router);
app.use('/api/v2', v2Router);

// Version in Header (empfohlen für interne APIs)
app.use('/api', (req, res, next) => {
  const version = req.headers['api-version'] || 'v1';
  req.apiVersion = version;
  next();
});

// Abwärtskompatibilität: V2 erweitert V1 (breaking changes vermeiden)
// V1: { id, name, price }
// V2: { id, name, price, currency, formatted_price } — additive Erweiterung

Connection Pooling

// PostgreSQL Connection Pool konfigurieren
const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 20, // Maximale gleichzeitige Verbindungen
  idleTimeoutMillis: 30000, // Inaktive Verbindungen nach 30s schließen
  connectionTimeoutMillis: 2000, // Timeout wenn kein Connection verfügbar
});

// Für sehr hohe Last: PgBouncer als Connection Pooler vor PostgreSQL
// PgBouncer kann Tausende gleichzeitige App-Verbindungen auf
// wenige DB-Verbindungen bündeln

Pagination richtig implementieren

// Cursor-basierte Pagination (skalierbar, konsistent)
app.get('/api/products', async (req, res) => {
  const { cursor, limit = 20 } = req.query;

  const where = cursor
    ? { id: { gt: cursor } } // Nur nach Cursor-ID
    : {};

  const products = await db.products.findMany({
    where,
    take: parseInt(limit) + 1, // +1 um zu prüfen ob es mehr gibt
    orderBy: { id: 'asc' },
  });

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

  res.json({
    data: items,
    pagination: {
      hasMore,
      nextCursor,
      limit: parseInt(limit),
    }
  });
});

Asynchrone Verarbeitung für lange Tasks

// Schwere Operationen in Queue auslagern (Bull + Redis)
import Queue from 'bull';
const exportQueue = new Queue('exports', process.env.REDIS_URL);

// API gibt sofort zurück
app.post('/api/reports/export', async (req, res) => {
  const job = await exportQueue.add({
    userId: req.user.id,
    reportType: req.body.reportType,
    filters: req.body.filters,
  });

  res.json({
    jobId: job.id,
    statusUrl: `/api/jobs/${job.id}`,
    message: 'Export gestartet, Sie erhalten eine E-Mail wenn fertig'
  });
});

// Worker verarbeitet Jobs asynchron
exportQueue.process(async (job) => {
  const { userId, reportType, filters } = job.data;
  const data = await generateReport(reportType, filters);
  const url = await uploadToS3(data);
  await sendEmailWithReport(userId, url);
});

Monitoring und Observability

// Prometheus-Metriken für API-Performance
import promClient from 'prom-client';

const httpRequestDuration = new promClient.Histogram({
  name: 'http_request_duration_seconds',
  help: 'HTTP request duration in seconds',
  labelNames: ['method', 'route', 'status_code'],
  buckets: [0.01, 0.05, 0.1, 0.3, 0.5, 1, 2, 5],
});

app.use((req, res, next) => {
  const end = httpRequestDuration.startTimer();
  res.on('finish', () => {
    end({
      method: req.method,
      route: req.route?.path || req.path,
      status_code: res.statusCode,
    });
  });
  next();
});

// Metrics-Endpunkt für Prometheus/Grafana
app.get('/metrics', async (req, res) => {
  res.set('Content-Type', promClient.register.contentType);
  res.end(await promClient.register.metrics());
});

Skalierbarkeit ist kein Feature, das man nachträglich hinzufügen kann — sie muss in die Architektur eingebaut sein. Die Techniken in diesem Leitfaden bilden eine solide Grundlage. Wenn Sie eine API bauen, die echtem Scale standhalten muss, sprechen Sie mit uns.

Nichts verpassen

JM

Javier Manzano

Leidenschaftlich für Technologie und Softwareentwicklung. Wir teilen Wissen und Erfahrungen, um anderen Entwicklern beim Wachsen zu helfen.

Hat Ihnen dieser Artikel gefallen?

Wenn Sie Hilfe bei Ihrem Entwicklungsprojekt brauchen, sind wir für Sie da.

Kostenloses Gespräch buchen →