Communitygithub.com

addyosmani/agent-skills

Leitet zu stabilem API- und Schnittstellendesign an. Verwende dies beim Entwerfen von APIs, Modulgrenzen oder öffentlichen Schnittstellen. Verwende dies 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 agent-skills?

Diese Fähigkeit ist ein umfassender Leitfaden zum Entwerfen stabiler, gut dokumentierter Schnittstellen, die schwer falsch zu verwenden sind. Sie ist für Entwickler und KI-Agenten von unschätzbarem Wert, die REST APIs, GraphQL-Schemas, Modulgrenzen, Komponenten-Props oder jede andere Oberfläche erstellen, über die Code mit anderem Code kommuniziert. Die wichtigsten Grundsätze sind: Vertrag vor Implementierung, konsistente Fehlersemantik, Validierung nur an Systemgrenzen (insbesondere bei der Behandlung von Drittanbieter-APIs als nicht vertrauenswürdige Daten), Bevorzugung von Hinzufügen gegenüber Änderungen (Erweiterung von Schnittstellen ohne Unterbrechung bestehender Nutzer) sowie Hyrum's Law (alle beobachtbaren Verhaltensweisen werden irgendwann von jemandem als Vertrag angesehen). Die Fähigkeit betont auch die Einmal-Version-Regel, um Diamantabhängigkeiten zu vermeiden, und die Bedeutung der Implementierung der Idempotenzschlüssel-Ehrerbietung. Sie bietet konkrete Codebeispiele in TypeScript und eignet sich für Plattformen wie Claude Code, Cursor und Codex.

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

In Ihrer bevorzugten KI fragen

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

Dokumentation

API- und Schnittstellendesign

Überblick

Gestalte stabile, gut dokumentierte Schnittstellen, die schwer falsch zu verwenden 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 anwenden

  • Entwerfen neuer API-Endpunkte
  • Definieren von Modulgrenzen oder Verträgen zwischen Teams
  • Erstellen von Komponenten-Prop-Schnittstellen
  • Einrichten eines Datenbankschemas, das die API-Form bestimmt
  • Ändern bestehender öffentlicher Schnittstellen

Kernprinzipien

Hyrums Gesetz

Bei einer ausreichenden Anzahl von Nutzern einer API wird jedes beobachtbare Verhalten deines Systems von irgendjemandem als abhängig betrachtet, unabhängig davon, was du im Vertrag versprichst.

Das bedeutet: Jedes öffentliche Verhalten – einschließlich undokumentierter Eigenheiten, Fehlermeldungstexte, Timing und Reihenfolge – wird zu einem De-facto-Vertrag, sobald Nutzer davon abhängen. Designimplikationen:

  • Sei bewusst, was du preisgibst. Jedes beobachtbare Verhalten ist eine potenzielle Verpflichtung.
  • Gib keine Implementierungsdetails preis. Wenn Nutzer es beobachten können, werden sie davon abhängen.
  • Plane die Einstellung bereits beim Design ein. Siehe deprecation-and-migration für die sichere Entfernung von Dingen, von denen Nutzer abhängen.
  • Tests reichen nicht aus. Selbst mit perfekten Vertragstests können „sichere“ Änderungen laut Hyrums Gesetz echte Nutzer brechen, die von undokumentiertem Verhalten abhängen.

Die Ein-Versionen-Regel

Vermeide es, Konsumenten zu zwingen, zwischen mehreren Versionen derselben Abhängigkeit oder API zu wählen. Diamant-Abhängigkeitsprobleme entstehen, wenn verschiedene Konsumenten unterschiedliche Versionen derselben Sache benötigen. Entwirf für eine Welt, in der immer nur eine Version existiert – erweitere statt zu gabeln.

1. Vertrag zuerst

Definiere die Schnittstelle, bevor du sie implementierst. Der Vertrag ist die Spezifikation – die Implementierung folgt.

// Definiere 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ähle eine Fehlerstrategie und verwende sie überall:

// REST: HTTP-Statuscodes + strukturierter Fehlertext
// Jede Fehlerantwort folgt der gleichen Form
interface APIError {
  error: {
    code: string;        // Maschinenlesbar: "VALIDATION_ERROR"
    message: string;     // Menschenlesbar: "Email is required"
    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 (interne Details niemals preisgeben)

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

3. An den Grenzen validieren

Vertraue internem Code. Validiere an den Systemgrenzen, an denen externe Eingaben eintreten:

// Validiere an der API-Grenze
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: 'Invalid task data',
        details: result.error.flatten(),
      },
    });
  }

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

Wo Validierung hingehört:

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

Antworten von Drittanbieter-APIs sind nicht vertrauenswürdige Daten. Validiere ihre Form und ihren Inhalt, bevor du sie in irgendeiner Logik, Darstellung oder Entscheidungsfindung verwendest. Ein kompromittierter oder fehlerhafter 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 deiner eigenen Datenbank stammen

4. Hinzufügen statt Ändern bevorzugen

Erweitere Schnittstellen, ohne bestehende Konsumenten 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 Konsumenten
  priority: number;         // Von string geändert – bricht bestehende Konsumenten
}

5. Vorhersagbare Namensgebung

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
AufzählungswerteUPPER_SNAKE"IN_PROGRESS", "COMPLETED"

6. Idempotenz-Schlüssel einhalten

Die Annahme eines Idempotency-Key ist der Vertrag. Die Einhaltung ist die Implementierung, und hier liegt der Hase im Pfeffer – ein Schlüssel, den der Server akzeptiert, aber nachlässig behandelt, ist schlimmer als gar kein Schlüssel, weil der Client nun glaubt, dass Wiederholungen sicher sind.

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

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

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

Der Schlüssel stammt 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);
}

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

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

Schütze die Nutzlast. Derselbe Schlüssel mit einem anderen Inhalt 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: 'idempotency key reused with a different payload' });
}

Entscheide, was mit einem laufenden Duplikat passiert. Die erste Anfrage läuft noch, wenn die zweite eintrifft – der häufigste Fall bei Wiederholungsstürmen:

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

Lass niemals den zweiten Aufrufer durch, weil der Erste „festzustecken scheint“. Ein steckengebliebener Versuch, dessen Ausgang unbekannt ist, ist genau der Moment, in dem Duplizieren am meisten kostet.

Jeder Aufruf hat drei Ergebnisse, nicht zwei: Erfolg, Fehlschlag und unbekannt. Ein Timeout sagt dir nichts darüber, ob der Effekt eingetreten ist. Zeichne die Absicht vor dem Aufruf auf, sodass ein Absturz zwischen dem Aufruf und der Antwort Beweise hinterlässt, dass etwas später aufgelöst werden muss – anstatt einer stillschweigend wiederholten Belastung.

Setze die Aufbewahrung von der längsten Wiederholungskette fest, nicht von den Speicherkosten. Schlüssel müssen jeden Pfad überdauern, der dieselbe Absicht erneut zustellen kann, einschließlich einer Dead-Letter-Warteschlange, die eine Woche später erneut abgespielt wird, und jedes Anbieter-Streitfensters. Ein 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              → Eine Aufgabe erstellen
GET    /api/tasks/:id          → Eine einzelne Aufgabe abrufen
PATCH  /api/tasks/:id          → Eine Aufgabe aktualisieren (teilweise)
DELETE /api/tasks/:id          → Eine Aufgabe löschen

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

Paginierung

Paginiere Listen-Endpunkte:

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

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

Filtern

Verwende Abfrageparameter für Filter:

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

Partielle Aktualisierungen (PATCH)

Akzeptiere Teilobjekte – aktualisiere nur das, was bereitgestellt wird:

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

TypeScript-Schnittstellenmuster

Verwende diskriminierte Unions 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 };

// Konsument erhält Type-Narrowing
function getStatusLabel(status: TaskStatus): string {
  switch (status.type) {
    case 'pending': return 'Pending';
    case 'in_progress': return `In progress (${status.assignee})`;
    case 'completed': return `Done on ${status.completedAt}`;
    case 'cancelled': return `Cancelled: ${status.reason}`;
  }
}

Ein-/Ausgabetrennung

// 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;
}

Verwende markierte Typen für IDs

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

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

Häufige Rationalisierungen

RationalisierungRealität
„Wir dokumentieren die API später“Die Typen SIND die Dokumentation. Definiere sie zuerst.
„Wir brauchen jetzt keine Paginierung“Du wirst sie brauchen, sobald jemand 100+ Elemente hat. Füge sie von Anfang an hinzu.
„PATCH ist kompliziert, lass uns einfach PUT verwenden“PUT erfordert jedes Mal das vollständige Objekt. PATCH ist das, was Clients tatsächlich wollen.
„Wir versionieren die API, wenn wir sie brauchen“Bahnbrechende Änderungen ohne Versionierung brechen Konsumenten. Entwerfe von Anfang an für Erweiterung.
„Niemand verwendet dieses undokumentierte Verhalten“Hyrums Gesetz: Wenn es beobachtbar ist, hängt jemand davon ab. Behandle jedes öffentliche Verhalten als Verpflichtung.
„Wir können einfach zwei Versionen verwalten“Mehrere Versionen vervielfachen die Wartungskosten und verursachen Diamant-Abhängigkeitsprobleme. Bevorzuge die Ein-Versionen-Regel.
„Interne APIs brauchen keine Verträge“Interne Konsumenten sind immer noch Konsumenten. Verträge verhindern Kopplung und ermöglichen parallele Arbeit.
„Die Annahme des Idempotency-Key-Headers ist genug“Der Header ist der Vertrag; das Speichern des Schlüssels gegen das Ergebnis ist die Implementierung. Ein Schlüssel, den du akzeptierst, aber nicht einhältst, sagt dem Client, dass Wiederholungen sicher sind, obwohl sie es nicht sind.
„Unsere Warteschlange garantiert Exactly-Once-Zustellung“Keine Warteschlange tut dies bei einem Consumer-Absturz – die Bestätigung des Brokers und dein Nebeneffekt befinden sich nicht in einer Transaktion. Entwerfe 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 zwischen Endpunkten
  • Validierung, die im gesamten internen Code verstreut ist, anstatt an Grenzen
  • Bahnbrechende Änderungen an vorhandenen Feldern (Typänderungen, Entfernungen)
  • Listen-Endpunkte 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 Idempotenz-Schlüssel gefolgt von einem INSERT – das ist ein Wettlauf, keine Absicherung
  • Ein Idempotenz-Schlüssel, der von einer UUID, einem Zeitstempel oder etwas anderem abgeleitet ist, das pro Versuch neu generiert wird
  • Derselbe Schlüssel mit einem anderen Anforderungstext akzeptiert, der stillschweigend die erste Antwort zurückgibt
  • Ein Schlüssel-Aufbewahrungsfenster, das kürzer ist als der längste Pfad, der die Anfrage erneut zustellen kann

Überprüfung

Nach dem Entwurf einer API:

  • Jeder Endpunkt hat typisierte Ein- und Ausgabeschemata
  • Fehlerantworten folgen einem einzigen konsistenten Format
  • Validierung erfolgt nur an Systemgrenzen
  • Listen-Endpunkte unterstützen Paginierung
  • Neue Felder sind additiv und optional (abwärtskompatibel)
  • Die Namensgebung folgt konsistenten Konventionen über alle Endpunkte hinweg
  • API-Dokumentation wird generiert (aus Typen oder OpenAPI-Spezifikation)

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

addyosmani/agent-skills

Prepares production launches. Use when preparing to deploy to production, or when asking what needs to be in place before shipping. Use when you need a pre-launch checklist, when setting up monitoring, when planning a staged rollout, or when you need a rollback strategy.

addyosmani/agent-skills

Builds production-quality, accessible, responsive user-facing UIs. Use when building or modifying interfaces and pages, creating components, implementing layouts, meeting WCAG accessibility requirements, managing state, or when the output needs to look and feel production-quality rather than AI-generated.

addyosmani/browser-testing-with-devtools

Tests in real browsers via Chrome DevTools MCP. Use when building or debugging anything that runs in a browser. Use when you need to inspect the DOM, capture console errors, analyze network requests, profile performance, or verify visual output with real runtime data. Requires the chrome-devtools MCP server to be configured.

addyosmani/ci-cd-and-automation

Automates CI/CD pipeline setup. Use when setting up or modifying build and deployment pipelines. Use when you need to automate quality gates, configure test runners in CI, or establish deployment strategies.

addyosmani/code-review-and-quality

Conducts multi-axis code review. Use before merging any change. Use when reviewing code written by yourself, another agent, or a human. Use when you need to assess code quality across multiple dimensions before it enters the main branch.

addyosmani/code-simplification

Simplifies code for clarity. Use when refactoring code for clarity without changing behavior. Use when code works but is harder to read, maintain, or extend than it should be. Use when reviewing code that has accumulated unnecessary complexity.

addyosmani/constraint-driven-development

Establishes a project's quality bar as a written contract and stops agents quietly lowering it. Interviews the user on which dimensions matter, supplies sane default thresholds when they have no number in mind, records everything in CONSTRAINTS.md, and watches the diff for a weakened bar — new @ts-ignore or eslint-disable suppressions, skipped or deleted tests, assertions stripped out, unimplemented stubs, thresholds edited down. Use when no quality bar is written down, when the user says "set up constraints" or "define our standards", when the user wants dimensions they care about — accessibility, web performance, coverage — set up as enforced constraints, when an agent keeps silencing checks or skipping tests to get to green, when you need a coverage or performance threshold and don't know what number to pick, or when an agent writes more code than anyone will read.

addyosmani/debugging-and-error-recovery

Guides systematic root-cause debugging. Use when tests fail, builds break, behavior doesn't match expectations, or you encounter any unexpected error. Use when you need a systematic approach to finding and fixing the root cause rather than guessing.

addyosmani/deprecation-and-migration

Manages deprecation and migration. Use when removing old systems, APIs, or features. Use when migrating users from one implementation to another. Use when deciding whether to maintain or sunset existing code.

addyosmani/documentation-and-adrs

Records decisions and documentation. Use when making architectural decisions, changing public APIs, shipping features, or when you need to record context that future engineers and agents will need to understand the codebase.

addyosmani/doubt-driven-development

Subjects every non-trivial decision to a fresh-context adversarial review before it stands. Use when correctness matters more than speed, when working in unfamiliar code, when stakes are high (production, security-sensitive logic, irreversible operations), or any time a confident output would be cheaper to verify now than to debug later.

addyosmani/git-workflow-and-versioning

Structures git workflow practices. Use when making any code change. Use when committing, branching, resolving conflicts, or when you need to organize work across multiple parallel streams.

addyosmani/idea-refine

Refines raw ideas into sharp, actionable concepts through structured divergent and convergent thinking. Use when an idea is still vague, when you need to stress-test assumptions before committing to a plan, or when you want to expand options before converging on one. Triggers on "ideate", "refine this idea", or "stress-test my plan".

addyosmani/incremental-implementation

Delivers changes incrementally. Use when implementing any feature or change that touches more than one file. Use when you're about to write a large amount of code at once, or when a task feels too big to land in one step.

addyosmani/interview-me

Extracts what the user actually wants instead of what they think they should want. Achieves this through one-question-at-a-time interview until ~95% confidence about the underlying intent. Use when an ask is underspecified ("build me X" without "for whom" or "why now"), when the user explicitly invokes ("interview me", "grill me", "are we sure?", "stress-test my thinking"), or when you catch yourself silently filling in ambiguous requirements before any plan, spec, or code exists.

addyosmani/observability-and-instrumentation

Instruments code so production behavior is visible and diagnosable. Use when adding logging, metrics, tracing, or alerting. Use when shipping any feature that runs in production and you need evidence it works. Use when production issues are reported but you can't tell what happened from the available data.

addyosmani/performance-optimization

Optimizes application performance. Use when performance requirements exist, when you suspect performance regressions, or when Core Web Vitals or load times need improvement. Use when profiling reveals bottlenecks that need fixing.

addyosmani/planning-and-task-breakdown

Breaks work into ordered tasks. Use when you have a spec or clear requirements and need to break work into implementable tasks. Use when a task feels too large to start, when you need to estimate scope, or when parallel work is possible.

addyosmani/security-and-hardening

Hardens code against vulnerabilities. Use when handling user input, authentication, data storage, or external integrations. Use when building any feature that accepts untrusted data, manages user sessions, or interacts with third-party services.

addyosmani/source-driven-development

Grounds every implementation decision in official documentation. Use when you want authoritative, source-cited code free from outdated patterns. Use when building with any framework or library where correctness matters.

Verwandte Skills