API-Design für Systeme, die länger leben als dein Team
Wie du APIs designst, die Team-Wechsel überleben. Protobuf als Schema-Quelle, tRPC für TypeScript-Stacks, GraphQL in Produktion, Breaking-Change-Management und Error-Contracts.
Die API, die ihren Schöpfer überlebt
Jede API startet mit einem Team, einem Client und einem Anwendungsfall. Zwei Jahre später hängen 5 Teams daran, 3 externe Partner integrieren sich, und die ursprünglichen Entwickler sind weg. Die Designentscheidungen der API sind jetzt permanent.
Wir haben APIs mit drei verschiedenen Strategien gebaut: Protobuf als Schema-Quelle für sprachübergreifende Systeme, tRPC für reine TypeScript-Stacks und GraphQL für komplexe Client-Anforderungen. Jede war die richtige Wahl für ihren Kontext. Dieser Artikel behandelt, wann du welche nutzen solltest und wie du APIs designst, die Team-Wechsel überleben.
Wie diese APIs in größere Systemdesigns passen, findest du in unserem System-Architecture-Guide und Software-Engineering-Guide.
Protobuf: Schema-Quelle für sprachübergreifende Systeme
Wenn dein System mehrere Sprachen umfasst (TypeScript API, Go Search Service, Python ML-Pipeline), brauchst du ein Schema-Format, das Typen für alle generiert. Protobuf macht genau das.
// proto/product.proto
syntax = "proto3";
package commerce.v1;
message Product {
string id = 1;
string name = 2;
string description = 3;
int32 price_cents = 4;
string currency = 5;
repeated string category_ids = 6;
ProductStatus status = 7;
google.protobuf.Timestamp created_at = 8;
}
enum ProductStatus {
PRODUCT_STATUS_UNSPECIFIED = 0;
PRODUCT_STATUS_DRAFT = 1;
PRODUCT_STATUS_ACTIVE = 2;
PRODUCT_STATUS_ARCHIVED = 3;
}
service ProductService {
rpc GetProduct(GetProductRequest) returns (Product);
rpc ListProducts(ListProductsRequest) returns (ListProductsResponse);
rpc CreateProduct(CreateProductRequest) returns (Product);
}
Warum Tags für die Weiterentwicklung wichtig sind
Protobuf-Felder werden durch Tag-Nummern (1, 2, 3...) identifiziert, nicht durch Namen. Das bedeutet: Du kannst ein Feld umbenennen, ohne bestehende Clients zu brechen. Du kannst neue Felder (neue Tag-Nummern) hinzufügen, ohne alte Clients zu brechen. Du kannst Felder als deprecated markieren, ohne sie zu entfernen.
// Sichere Weiterentwicklung: Feld hinzufügen
message Product {
string id = 1;
string name = 2;
string description = 3;
int32 price_cents = 4;
string currency = 5;
repeated string category_ids = 6;
ProductStatus status = 7;
google.protobuf.Timestamp created_at = 8;
string sku = 9; // NEU: hinzugefügt, ohne bestehende Clients zu brechen
string brand = 10; // NEU: alte Clients ignorieren unbekannte Felder einfach
}
Regeln für sichere Protobuf-Evolution:
- Niemals eine Tag-Nummer wiederverwenden (auch nicht nach Entfernung eines Feldes)
- Niemals den Typ eines Feldes ändern
- Neue Felder mit neuen Tag-Nummern hinzufügen
- Veraltete Felder mit
reservedmarkieren, um versehentliche Wiederverwendung zu verhindern
Code-Generierung
# TypeScript, Go und Python aus derselben .proto-Datei generieren
protoc --ts_out=./gen/ts --go_out=./gen/go --python_out=./gen/py proto/*.proto
Eine Schema-Datei generiert typsichere Clients und Server in jeder Sprache. Schema-Änderungen sind ein Pull Request. Code-Generierung ist ein CI-Schritt. Typ-Fehler sind Compile-Errors, keine Runtime-Bugs.
tRPC: Wenn dein gesamter Stack TypeScript ist
Wenn dein API-Server und alle Clients TypeScript sind, eliminiert tRPC die Schema-Schicht komplett. Typen fließen vom Server zum Client zur Compile-Zeit. Keine Code-Generierung, keine Schema-Dateien, keine OpenAPI-Spec.
// Server: Router mit typisierten Prozeduren definieren
import { router, publicProcedure, protectedProcedure } from './trpc';
import { z } from 'zod';
export const productRouter = router({
list: publicProcedure
.input(z.object({
cursor: z.string().optional(),
limit: z.number().min(1).max(100).default(20),
category: z.string().optional(),
}))
.query(async ({ input, ctx }) => {
return ctx.productService.list(input);
}),
create: protectedProcedure
.input(z.object({
name: z.string().min(1).max(200),
price: z.number().positive(),
description: z.string().optional(),
}))
.mutation(async ({ input, ctx }) => {
return ctx.productService.create(ctx.tenantId, input);
}),
});
// Client: vollständige Typ-Inferenz, keine Code-Generierung
const products = await trpc.product.list.query({
limit: 10,
category: 'electronics',
});
// products ist voll typisiert: { items: Product[], nextCursor?: string }
Wann tRPC am besten funktioniert
- Alle Clients sind TypeScript (Web-App, React Native, Node.js Services)
- Die API ist intern (nicht für Drittparteien freigegeben)
- Schnelle Iteration wichtiger ist als formelle API-Contracts
- Das Team ist klein und arbeitet zusammen (Änderungen an Server und Client passieren gleichzeitig)
Wann tRPC nicht funktioniert
- Externe Partner müssen integrieren (sie brauchen OpenAPI/Swagger-Docs)
- Clients sind in anderen Sprachen (mobile native, Go, Python)
- Du brauchst API-Versionierung für Rückwärtskompatibilität
- Die API ist ein öffentliches Produkt
Wir setzen tRPC mit Hono für interne TypeScript-APIs ein. Schau dir unseren TypeScript-Backends-Guide für den vollständigen Stack-Vergleich an.
GraphQL: Für komplexe Client-Anforderungen
GraphQL glänzt, wenn Clients flexible Abfragen brauchen: Verschiedene Seiten brauchen verschiedene Daten-Subsets, Mobile braucht weniger Daten als Web, und das Client-Team will Queries iterieren, ohne Backend-Änderungen.
# Client fragt genau das ab, was er braucht
query ProductPage($slug: String!) {
product(slug: $slug) {
id
name
price
images { url alt }
reviews(first: 5, status: APPROVED) {
items { rating body customerName }
totalItems
}
relatedProducts(first: 4) {
id name price images { url }
}
}
}
Production-GraphQL-Patterns
Persisted Queries: Erlaube keine beliebigen Queries in Produktion. Clients senden einen Query-Hash, der Server schlägt die Query in einer Registry nach. Das verhindert Query-Missbrauch und ermöglicht Caching.
Depth Limiting: Ohne Limits kann ein Client eine tief verschachtelte Query senden, die jede Tabelle in deiner Datenbank joint.
apiOptions: {
middleware: [depthLimit(10)],
shopApiPlayground: false, // In Produktion deaktivieren
}
N+1-Prävention: Nutze DataLoader, um Datenbankabfragen zu bündeln. Ohne das macht eine Query, die 20 Produkte mit ihren Kategorien abruft, 20 einzelne Kategorie-Abfragen.
// DataLoader bündelt N einzelne Queries zu 1
const categoryLoader = new DataLoader(async (ids: string[]) => {
const categories = await categoryRepo.findByIds(ids);
return ids.map(id => categories.find(c => c.id === id));
});
Komplexitätsanalyse: Weise jedem Feld Kosten zu. Lehne Queries ab, die ein Komplexitätsbudget überschreiten.
Wie wir GraphQL in Vendure Commerce einsetzen, findest du in unserem Vendure-Production-Guide.
Breaking Changes: Tag-basierte Evolution vs. URL-Versionierung
| Strategie | Wie es funktioniert | Am besten für |
|---|---|---|
| Tag-basiert (Protobuf) | Felder mit neuen Tags hinzufügen, alte Clients ignorieren sie | Sprachübergreifend, gRPC |
URL-Versionierung (/v1/, /v2/) | Separate Endpunkte pro Version | REST-APIs mit externen Konsumenten |
Header-Versionierung (Accept: application/vnd.api.v2+json) | Gleiche URL, Version im Header | REST-APIs, die saubere URLs wollen |
| GraphQL (keine Versionierung) | Felder hinzufügen, alte mit @deprecated markieren | GraphQL-APIs |
| tRPC (keine Versionierung) | Typen entwickeln sich mit der Codebase | Interne TypeScript-APIs |
Für die meisten internen APIs: Vermeide URL-Versionierung. Sie verdoppelt deine Wartungsfläche. Füge neue Felder hinzu, markiere alte als deprecated, entferne sie, wenn alle Clients migriert haben.
Für externe APIs (Drittanbieter-Integrationen, öffentliche APIs) ist URL-Versionierung sicherer, weil du nicht kontrollieren kannst, wann Clients updaten.
Error Contracts
Errors sind Teil des API-Contracts. Clients brauchen strukturierte Fehler, auf die sie reagieren können, keine String-Nachrichten, die sie mit Regex parsen.
// Strukturierte Fehlerantwort
interface ApiError {
code: string; // Maschinenlesbar: "PRODUCT_NOT_FOUND", "INSUFFICIENT_STOCK"
message: string; // Menschenlesbar: "Product with ID xyz not found"
details?: object; // Zusätzlicher Kontext für Debugging
requestId: string; // Korrelations-ID für Support
}
// Beispielantworten
// 404
{
"code": "PRODUCT_NOT_FOUND",
"message": "Product with ID prod_123 not found",
"requestId": "req_abc456"
}
// 409
{
"code": "INSUFFICIENT_STOCK",
"message": "Only 2 units available, requested 5",
"details": { "available": 2, "requested": 5 },
"requestId": "req_def789"
}
// 422
{
"code": "VALIDATION_ERROR",
"message": "Invalid input",
"details": {
"fields": [
{ "field": "price", "error": "must be positive" },
{ "field": "name", "error": "must not be empty" }
]
},
"requestId": "req_ghi012"
}
Das code-Feld ist der Contract. Clients nutzen es als Switch. Die message ist für Menschen. Die details liefern Kontext. Die requestId ermöglicht dem Support, genau den richtigen Request in den Logs zu finden.
API Governance
Wenn die API wächst, brauchst du Governance, um Inkonsistenz zu verhindern:
| Regel | Warum |
|---|---|
| Alle Endpunkte erfordern Authentifizierung | Keine versehentlich öffentlichen Endpunkte |
| Alle Mutations erfordern Idempotency Keys | Netzwerk-Retries erzeugen keine Duplikate |
Alle Antworten enthalten requestId | Jeder Request ist nachverfolgbar |
| Alle Fehler nutzen das strukturierte Format | Clients können Fehler programmatisch behandeln |
| Alle List-Endpunkte unterstützen Pagination | Keine unbegrenzten Queries |
| Breaking Changes erfordern Review | Eine Person kann nicht alle Consumer brechen |
| Deprecations erfordern einen Migrations-Zeitplan | "Deprecated" ohne Deadline heißt "wird nie entfernt" |
Häufige Fehler
-
REST vs. GraphQL als Identität. Wähle basierend auf Client-Anforderungen, nicht Team-Präferenz. REST für einfaches CRUD mit externen Konsumenten. GraphQL für flexible Queries mit Frontend-Teams. tRPC für internes TypeScript. Protobuf für sprachübergreifend.
-
Kein Error Contract. String-Fehlermeldungen, die sich mit jedem Release ändern, brechen jeden Client, der versucht, sie zu behandeln.
-
Unbegrenzte List-Endpunkte. Ein Endpunkt, der alle 50.000 Produkte in einer Antwort zurückgibt, crasht Clients und überlastet deine Datenbank. Pagination ist Pflicht.
-
Interne APIs versionieren. Wenn du alle Clients kontrollierst, entwickle die API in-place weiter. Versionierung verdoppelt die Wartung ohne Nutzen.
-
Kein Deprecation-Zeitplan. Ein Feld als
@deprecatedmarkieren ohne Entfernungsdatum heißt, es bleibt für immer. Setze ein Datum, informiere die Consumer, entferne es. -
GraphQL ohne Depth Limits. Eine uneingeschränkte GraphQL-API ist ein Denial-of-Service-Vektor. Begrenze Tiefe, Komplexität und Query-Kosten.
Wichtigste Erkenntnisse
-
Protobuf für sprachübergreifende Systeme. Tag-basierte Evolution, Code-Generierung, Wire-Format-Effizienz. Das Schema ist der Contract.
-
tRPC für reine TypeScript-Stacks. Null Code-Generierung, volle Typ-Inferenz, schnellste Iterations-Geschwindigkeit. Funktioniert aber nur, wenn alle Clients TypeScript sind.
-
GraphQL für flexible Client-Anforderungen. Clients fragen genau das ab, was sie brauchen. Aber füge Depth Limits, Persisted Queries und Komplexitätsanalyse für Produktion hinzu.
-
Error Contracts sind genauso wichtig wie Erfolgs-Contracts. Maschinenlesbare Codes, menschenlesbare Nachrichten, Korrelations-IDs und strukturierte Details.
-
In-place weiterentwickeln für interne APIs, versionieren für externe. Felder hinzufügen ist sicher. Felder entfernen erfordert einen Migrations-Zeitplan. URL-Versionierung ist der letzte Ausweg.
Wir designen APIs als Teil unserer Webentwicklung und Custom-Software-Praxis. Wenn du Hilfe mit API-Architektur brauchst, sprich mit unserem Team oder frage ein Angebot an.
Behandelte Themen
Verwandte Guides
Systemarchitektur & Skalierbarkeit
Leitfaden zum Design langlebiger Systeme. Erfahre ueber Architekturmuster, API-Design, Authentifizierung, Echtzeit-Infrastruktur und Skalierung.
Guide lesenUnternehmenshandbuch zu Agentischen KI-Systemen
Technischer Leitfaden zu agentischen KI-Systemen in Unternehmen. Erfahre mehr ueber Architektur, Faehigkeiten und Anwendungen autonomer KI-Agenten.
Guide lesenAgentic Commerce: Wie du KI-Agenten sicher einkaufen lässt
Wie du gesteuerten, KI-initiierten Handel designst. Policy Engines, HITL-Freigabe-Gates, HMAC-Quittungen, Idempotenz, Tenant-Scoping und das vollständige Agentic Checkout Protocol.
Guide lesenBereit, produktionsreife KI-Systeme zu bauen?
Unser Team ist spezialisiert auf produktionsreife KI-Systeme. Lass uns besprechen, wie wir deinem Unternehmen helfen können.
Gespräch starten