Communitygithub.com

YOUSSFE9/bolt-skills

Leitet zu stabilem API- und Interface-Design an. Verwenden Sie es beim Entwerfen von APIs, Modulgrenzen oder öffentlichen Schnittstellen. Verwenden Sie es beim Erstellen von REST- oder GraphQL-Endpunkten, beim Definieren von Typverträgen zwischen Modulen oder beim Festlegen von Grenzen zwischen Frontend und Backend.

Was ist bolt-skills?

Diese Fähigkeit bietet einen umfassenden Leitfaden zum Entwurf stabiler, gut dokumentierter und schwer zu missbrauchender APIs und Schnittstellen. Sie deckt ein breites Spektrum ab, von REST-APIs über GraphQL-Schemata bis hin zu Modulgrenzen und Komponenten-Props. Der Leitfaden legt Prinzipien wie Hyrums Gesetz (alle beobachtbaren Verhaltensweisen werden zur Abhängigkeit), die Ein-Versionen-Regel (keine parallelen Versionen) und die Vertrag-zuerst-Methodik dar. Zu den praktischen Anleitungen gehören konsistente Fehlersemantik, Validierung an Systemgrenzen, Bevorzugung von Erweiterungen gegenüber Änderungen und vorhersagbare Namenskonventionen. Ein besonderer Schwerpunkt liegt auf der korrekten Implementierung von Idempotenzschlüsseln, wobei erläutert wird, wie Schlüssel abgeleitet, atomar beansprucht und Nutzdaten geschützt werden. Die Fähigkeit ist für Entwickler konzipiert, die mit Agent-Tools wie Claude Code, Cursor oder Codex arbeiten, und wird als SKILL.md-Datei bereitgestellt. Sie ist plattformunabhängig und in TypeScript und Englisch verfasst. Die Implementierung umfasst Codebeispiele, Tabellen und Checklisten, um die Regeln in der täglichen Praxis anwendbar zu machen.

Funktioniert mit~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/YOUSSFE9/bolt-skills/tree/HEAD/api-and-interface-design

In Ihrer bevorzugten KI fragen

Öffnet einen neuen Chat, in dem dieser Agent-Skill bereits geladen ist.

Dokumentation

API- und Interface-Design

Überblick

Entwerfen Sie stabile, gut dokumentierte Schnittstellen, die schwer zu missbrauchen sind. Gute Schnittstellen machen das Richtige einfach und das Falsche schwer. Dies gilt für REST-APIs, GraphQL-Schemata, Modulgrenzen, Komponenten-Props und jede Oberfläche, über die ein Codeabschnitt mit einem anderen kommuniziert.

Wann verwenden

  • Entwerfen neuer API-Endpunkte
  • Definieren von Modulgrenzen oder Verträgen zwischen Teams
  • Erstellen von Komponenten-Prop-Schnittstellen
  • Festlegen von Datenbankschemata, die die API-Form bestimmen
  • Ändern vorhandener öffentlicher Schnittstellen

Kernprinzipien

Hyrums Gesetz

Bei einer ausreichenden Anzahl von Benutzern einer API werden alle beobachtbaren Verhaltensweisen Ihres Systems von irgendjemandem als Abhängigkeit betrachtet, unabhängig davon, was Sie im Vertrag versprechen.

Das bedeutet: Jedes öffentliche Verhalten – einschließlich undokumentierter Eigenheiten, Fehlermeldungstexte, Zeitverhalten und Reihenfolge – wird zu einem De-facto-Vertrag, sobald Benutzer darauf angewiesen sind. Design-Implikationen:

  • Seien Sie bewusst, was Sie offenlegen. Jedes beobachtbare Verhalten ist eine potenzielle Verpflichtung.
  • Geben Sie keine Implementierungsdetails preis. Wenn Benutzer es beobachten können, werden sie sich darauf verlassen.
  • Planen Sie die Abschaffung bereits beim Design. Siehe deprecation-and-migration für die sichere Entfernung von Dingen, von denen Benutzer abhängen.
  • Tests sind nicht genug. Selbst mit perfekten Vertragstests können „sichere“ Änderungen gemäß Hyrums Gesetz echte Benutzer brechen, die von undokumentiertem Verhalten abhängen.

Die Ein-Versionen-Regel

Vermeiden Sie es, Verbraucher zu zwingen, zwischen mehreren Versionen derselben Abhängigkeit oder API zu wählen. Diamant-Abhängigkeitsprobleme entstehen, wenn verschiedene Verbraucher unterschiedliche Versionen derselben Sache benötigen. Entwerfen Sie für eine Welt, in der immer nur eine Version existiert – erweitern Sie, anstatt zu verzweigen.

1. Vertrag zuerst

Definieren Sie die Schnittstelle, bevor Sie sie implementieren. Der Vertrag ist die Spezifikation – die Implementierung folgt.

// Definieren Sie zuerst den Vertrag
interface TaskAPI {
  // Erstellt eine Aufgabe und gibt die erstellte Aufgabe mit serverseitig generierten Feldern zurück
  createTask(input: CreateTaskInput): Promise<Task>;

  // Gibt paginierte Aufgaben zurück, die den Filtern entsprechen
  listTasks(params: ListTasksParams): Promise<PaginatedResult<Task>>;

  // Gibt eine einzelne Aufgabe zurück oder wirft NotFoundError
  getTask(id: string): Promise<Task>;

  // Partielle Aktualisierung – nur bereitgestellte Felder ändern sich
  updateTask(id: string, input: UpdateTaskInput): Promise<Task>;

  // Idempotentes Löschen – erfolgreich, auch wenn bereits gelöscht
  deleteTask(id: string): Promise<void>;
}

2. Konsistente Fehlersemantik

Wählen Sie eine Fehlerstrategie und verwenden Sie sie überall:

// REST: HTTP-Statuscodes + strukturierter Fehlerbody
// Jede Fehlerantwort folgt derselben Form
interface APIError {
  error: {
    code: string;        // Maschinenlesbar: "VALIDATION_ERROR"
    message: string;     // Menschenlesbar: "E-Mail ist erforderlich"
    details?: unknown;   // Zusätzlicher Kontext, wenn hilfreich
  };
}

// Statuscode-Zuordnung
// 400 → Client hat ungültige Daten gesendet
// 401 → Nicht authentifiziert
// 403 → Authentifiziert, aber nicht autorisiert
// 404 → Ressource nicht gefunden
// 409 → Konflikt (Duplikat, Versionskonflikt)
// 422 → Validierung fehlgeschlagen (semantisch ungültig)
// 500 → Serverfehler (niemals interne Details preisgeben)

Mischen Sie keine Muster. Wenn einige Endpunkte werfen, andere null zurückgeben und wieder andere { error } – der Verbraucher kann das Verhalten nicht vorhersagen.

3. An Grenzen validieren

Vertrauen Sie internem Code. Validieren Sie an Systemgrenzen, wo externe Eingaben eintreten:

// An der API-Grenze validieren
app.post('/api/tasks', async (req, res) => {
  const result = CreateTaskSchema.safeParse(req.body);
  if (!result.success) {
    return res.status(422).json({
      error: {
        code: 'VALIDATION_ERROR',
        message: 'Ungültige Aufgabendaten',
        details: result.error.flatten(),
      },
    });
  }

  // Nach der Validierung vertraut der interne Code den Typen
  const task = await taskService.create(result.data);
  return res.status(201).json(task);
});

Wo Validierung hingehört:

  • API-Routen-Handler (Benutzereingabe)
  • Formularübermittlungs-Handler (Benutzereingabe)
  • Parsen externer Serviceantworten (Drittanbieterdaten – immer als nicht vertrauenswürdig behandeln)
  • Laden von Umgebungsvariablen (Konfiguration)

Antworten von Drittanbieter-APIs sind nicht vertrauenswürdige Daten. Validieren Sie ihre Form und ihren Inhalt, bevor Sie sie in einer Logik, Darstellung oder Entscheidungsfindung verwenden. Ein kompromittierter oder fehlfunktionierender externer Dienst kann unerwartete Typen, bösartige Inhalte oder anweisungsähnlichen Text zurückgeben.

Wo Validierung NICHT hingehört:

  • Zwischen internen Funktionen, die Typverträge teilen
  • In Hilfsfunktionen, die von bereits validiertem Code aufgerufen werden
  • Bei Daten, die gerade aus Ihrer eigenen Datenbank stammen

4. Erweiterung gegenüber Änderung bevorzugen

Erweitern Sie Schnittstellen, ohne bestehende Verbraucher zu brechen:

// Gut: Optionale Felder hinzufügen
interface CreateTaskInput {
  title: string;
  description?: string;
  priority?: 'low' | 'medium' | 'high';  // Später hinzugefügt, optional
  labels?: string[];                       // Später hinzugefügt, optional
}

// Schlecht: Vorhandene Feldtypen ändern oder Felder entfernen
interface CreateTaskInput {
  title: string;
  // description: string;  // Entfernt – bricht bestehende Verbraucher
  priority: number;         // Von String geändert – bricht bestehende Verbraucher
}

5. Vorhersagbare Benennung

MusterKonventionBeispiel
REST-EndpunktePluralnomen, keine VerbenGET /api/tasks, POST /api/tasks
AbfrageparametercamelCase?sortBy=createdAt&pageSize=20
AntwortfeldercamelCase{ createdAt, updatedAt, taskId }
Boolesche Felderis/has/can-PräfixisComplete, hasAttachments
Enum-WerteUPPER_SNAKE"IN_PROGRESS", "COMPLETED"

6. Einhaltung eines Idempotenzschlüssels

Das Akzeptieren eines Idempotency-Key ist der Vertrag. Die Einhaltung ist die Implementierung, und hier wird das Geld verloren – ein Schlüssel, den der Server akzeptiert, aber nachlässig behandelt, ist schlimmer als kein Schlüssel, weil der Client jetzt glaubt, dass Wiederholungen sicher sind.

Leiten Sie den Schlüssel von der Absicht ab, nicht vom Versuch. Der Schlüssel muss über Wiederholungen einer Absicht stabil und über unterschiedliche Absichten hinweg unterschiedlich sein:

crypto.randomUUID()                    // ✗ neuer Schlüssel pro Versuch – jede Wiederholung ist eine neue Belastung
`${userId}:${amount}`                  // ✗ zwei legitime $50-Belastungen fallen zu einer zusammen
`${orderId}:${Date.now()}`             // ✗ ein Zeitstempel ist randomUUID() mit einem Hut

req.headers['idempotency-key']         // ✓ Client generiert einmal, verwendet bei Wiederholung erneut
`charge:v1:${orderId}`                 // ✓ abgeleitet von einer unveränderlichen Kennung

Der Schlüssel kommt vom Client oder vom auslösenden Ereignis – niemals von der Schicht, die die Wiederholung durchführt.

Atomar beanspruchen. Eine Prüfung gefolgt von einer Aktion ist ein Wettlauf:

// ✗ TOCTOU: Zwei gleichzeitige Wiederholungen lesen beide „nicht gesehen“, beide belasten
if (!(await db.exists(key))) {
  await chargeCard(amount);
  await db.insert(key);
}

// ✓ Lassen Sie die eindeutige Einschränkung den Gewinner bestimmen
try {
  await db.insert({ key, state: 'in_progress', requestHash });
} catch (e) {
  if (isUniqueViolation(e)) return replayOrReject(key);
  throw e;
}
const result = await chargeCard(amount);
await db.update({ key, state: 'succeeded', response: result });

Die eindeutige Einschränkung ist der Mechanismus. Ein Speicher, der die Eindeutigkeit nicht in einem Vorgang erzwingen kann, kann dies nicht unterstützen.

Schützen Sie die Nutzlast. Derselbe Schlüssel mit einem anderen Body ist ein Client-Fehler und muss laut scheitern, anstatt die erste Antwort auf eine zweite Anfrage zu liefern:

if (existing.requestHash !== hash(req.body)) {
  return res.status(422).json({ error: 'Idempotenzschlüssel mit einer anderen Nutzlast wiederverwendet' });
}

Entscheiden Sie, was ein laufendes Duplikat bekommt. Die erste Anfrage läuft noch, wenn die zweite eintrifft – der häufigste Fall bei Wiederholungsstürmen:

StrategieAntwortVerwenden, wenn
Ablehnen409 ConflictClient kann später wiederholen; am einfachsten und sichersten
WartenAuf das Ergebnis blockieren, begrenztAufrufer benötigt es synchron
Ausstehend zurückgeben202 + Status-URLLanglaufende Effekte

Lassen Sie niemals den zweiten Aufrufer durch, weil der erste „festzustecken scheint“. Ein stockender Versuch, dessen Schicksal unbekannt ist, ist genau der Moment, in dem Duplizieren am meisten kostet.

Jeder Aufruf hat drei Ergebnisse, nicht zwei: Erfolg, Fehler und unbekannt. Ein Timeout sagt Ihnen nichts darüber, ob der Effekt angewendet wurde. Zeichnen Sie die Absicht vor dem Aufruf auf, damit ein Absturz zwischen dem Aufruf und der Antwort Beweise hinterlässt, dass später etwas aufgelöst werden muss – anstatt einer stillschweigend wiederholten Belastung.

Legen Sie die Aufbewahrungsdauer anhand der längsten Wiederholungskette fest, nicht anhand der Speicherkosten. Schlüssel müssen jeden Pfad überdauern, der dieselbe Absicht erneut zustellen kann, einschließlich einer eine Woche später wiederholten Dead-Letter-Queue und eines beliebigen Anbieter-Streitfensters. Eine 24-Stunden-Schlüssel-TTL hinter einer 7-Tage-DLQ ist ein Duplikat, das nur darauf wartet, zu passieren.

REST-API-Muster

Ressourcendesign

GET    /api/tasks              → Aufgaben auflisten (mit Abfrageparametern zum Filtern)
POST   /api/tasks              → Aufgabe erstellen
GET    /api/tasks/:id          → Einzelne Aufgabe abrufen
PATCH  /api/tasks/:id          → Aufgabe aktualisieren (teilweise)
DELETE /api/tasks/:id          → Aufgabe löschen

GET    /api/tasks/:id/comments → Kommentare zu einer Aufgabe auflisten (Unterressource)
POST   /api/tasks/:id/comments → Kommentar zu einer Aufgabe hinzufügen

Paginierung

Paginieren Sie Listenendpunkte:

// Anfrage
GET /api/tasks?page=1&pageSize=20&sortBy=createdAt&sortOrder=desc

// Antwort
{
  "data": [...],
  "pagination": {
    "page": 1,
    "pageSize": 20,
    "totalItems": 142,
    "totalPages": 8
  }
}

Filtern

Verwenden Sie Abfrageparameter für Filter:

GET /api/tasks?status=in_progress&assignee=user123&createdAfter=2025-01-01

Teilweise Aktualisierungen (PATCH)

Akzeptieren Sie Teilobjekte – aktualisieren Sie nur das, was bereitgestellt wird:

// Nur der Titel ändert sich, alles andere bleibt erhalten
PATCH /api/tasks/123
{ "title": "Aktualisierter Titel" }

TypeScript-Interface-Muster

Verwenden Sie diskriminierte Unionen für Varianten

// Gut: Jede Variante ist explizit
type TaskStatus =
  | { type: 'pending' }
  | { type: 'in_progress'; assignee: string; startedAt: Date }
  | { type: 'completed'; completedAt: Date; completedBy: string }
  | { type: 'cancelled'; reason: string; cancelledAt: Date };

// Verbraucher erhält Type Narrowing
function getStatusLabel(status: TaskStatus): string {
  switch (status.type) {
    case 'pending': return 'Ausstehend';
    case 'in_progress': return `In Bearbeitung (${status.assignee})`;
    case 'completed': return `Erledigt am ${status.completedAt}`;
    case 'cancelled': return `Abgebrochen: ${status.reason}`;
  }
}

Eingabe/Ausgabe-Trennung

// Eingabe: Was der Aufrufer bereitstellt
interface CreateTaskInput {
  title: string;
  description?: string;
}

// Ausgabe: Was das System zurückgibt (enthält serverseitig generierte Felder)
interface Task {
  id: string;
  title: string;
  description: string | null;
  createdAt: Date;
  updatedAt: Date;
  createdBy: string;
}

Verwenden Sie Markentypen für IDs

type TaskId = string & { readonly __brand: 'TaskId' };
type UserId = string & { readonly __brand: 'UserId' };

// Verhindert, dass versehentlich eine UserId übergeben wird, wo eine TaskId erwartet wird
function getTask(id: TaskId): Promise<Task> { ... }

Häufige Rechtfertigungen

RechtfertigungRealität
"Wir dokumentieren die API später"Die Typen SIND die Dokumentation. Definieren Sie sie zuerst.
"Wir brauchen jetzt keine Paginierung"Sie werden sie brauchen, sobald jemand 100+ Elemente hat. Fügen Sie sie von Anfang an hinzu.
"PATCH ist kompliziert, verwenden wir einfach PUT"PUT erfordert jedes Mal das vollständige Objekt. PATCH ist das, was Clients tatsächlich wollen.
"Wir versionieren die API, wenn es nötig ist"Breaking Changes ohne Versionierung brechen Verbraucher. Entwerfen Sie von Anfang an für Erweiterung.
"Niemand verwendet dieses undokumentierte Verhalten"Hyrums Gesetz: Wenn es beobachtbar ist, hängt jemand davon ab. Behandeln Sie jedes öffentliche Verhalten als Verpflichtung.
"Wir können einfach zwei Versionen pflegen"Mehrere Versionen vervielfachen den Wartungsaufwand und erzeugen Diamant-Abhängigkeitsprobleme. Bevorzugen Sie die Ein-Versionen-Regel.
"Interne APIs brauchen keine Verträge"Interne Verbraucher sind immer noch Verbraucher. Verträge verhindern Kopplung und ermöglichen parallele Arbeit.
"Das Akzeptieren des Idempotency-Key-Headers ist genug"Der Header ist der Vertrag; das Speichern des Schlüssels mit dem Ergebnis ist die Implementierung. Ein Schlüssel, den Sie akzeptieren, aber nicht einhalten, sagt dem Client, dass Wiederholungen sicher sind, obwohl sie es nicht sind.
"Unsere Warteschlange garantiert Exactly-Once-Zustellung"Keine Warteschlange tut das bei einem Consumer-Absturz – der Broker-Ack und Ihr Side-Effekt befinden sich nicht in einer Transaktion. Entwerfen Sie für At-Least-Once mit idempotenter Verarbeitung.
"Doppelte Anfragen sind selten"Sie sind korreliert. Wiederholungen steigen genau dann an, wenn eine Abhängigkeit beeinträchtigt ist – der Moment, in dem Duplikate am wahrscheinlichsten und am teuersten sind.

Warnsignale

  • Endpunkte, die je nach Bedingungen unterschiedliche Formen zurückgeben
  • Inkonsistente Fehlerformate über Endpunkte hinweg
  • Validierung, die im gesamten internen Code verstreut ist, anstatt an Grenzen
  • Breaking Changes an vorhandenen Feldern (Typänderungen, Entfernungen)
  • Listenendpunkte ohne Paginierung
  • Verben in REST-URLs (/api/createTask, /api/getUsers)
  • Antworten von Drittanbieter-APIs, die ohne Validierung oder Bereinigung verwendet werden
  • Ein SELECT für einen Idempotenzschlüssel, gefolgt von einem INSERT – das ist ein Wettlauf, keine Absicherung
  • Ein Idempotenzschlüssel, der von einer UUID, einem Zeitstempel oder etwas anderem abgeleitet wird, das pro Versuch neu generiert wird
  • Derselbe Schlüssel mit einem anderen Anforderungsbody akzeptiert, der stillschweigend die erste Antwort zurückgibt
  • Ein Schlüssel-Aufbewahrungsfenster, das kürzer ist als der längste Pfad, der die Anforderung erneut zustellen kann

Überprüfung

Nach dem Entwurf einer API:

  • Jeder Endpunkt hat typisierte Eingabe- und Ausgabeschemata
  • Fehlerantworten folgen einem einzigen konsistenten Format
  • Validierung findet nur an Systemgrenzen statt
  • Listenendpunkte unterstützen Paginierung
  • Neue Felder sind additiv und optional (abwärtskompatibel)
  • Die Benennung folgt konsistenten Konventionen über alle Endpunkte hinweg
  • Die API-Dokumentation ist auf dem neuesten Stand
  • Idempotenzschlüssel werden atomar beansprucht, Nutzlasten werden hash-überprüft, und das Aufbewahrungsfenster deckt die längste Wiederholungskette ab

Individual skills in this repo

This repo contains 1 individual skill — each has its own dedicated page.

Verwandte Skills