Skills Agentes

Api And Interface Design

Guía para diseñar APIs e interfaces estables. Úsalo al diseñar APIs, límites entre módulos, endpoints REST o GraphQL, o contratos de tipos entre frontend y backend.

Estrellas
87.7k

en todo el repo

Actividad
60

0–100, la ruta de este skill

Actualizado
hace 4 días

último commit aquí

Commits
1

últimos 90 días

Contexto
3.7k tok

63 tok en reposo

Paquete
1 archivo

15 KB

Instalar

Funciona con cualquier agente que lea SKILL.md

npx -y skills add addyosmani/agent-skills --skill api-and-interface-design --agent claude-code

Se instala solo en este repositorio.

Qué hace

  • Aplica principios de contrato primero, semántica de errores consistente y validación en los límites del sistema
  • Define patrones REST (paginación, filtrado, PATCH parcial) y patrones TypeScript (uniones discriminadas, tipos con marca)
  • Guía la implementación correcta de claves de idempotencia (Idempotency-Key), evitando condiciones de carrera y duplicados
  • Aplica la Ley de Hyrum y la regla de una sola versión para evitar romper consumidores existentes
  • Incluye una checklist de verificación tras diseñar una API

Úsalo cuando

  • Diseñando nuevos endpoints de API
  • Definiendo límites o contratos entre módulos o equipos
  • Creando interfaces de props de componentes
  • Cambiando interfaces públicas existentes

No lo uses cuando

    Qué lo activa

    Di cualquiera de estas frases y el agente debería cargar este skill.

    • Diseña los endpoints REST para el recurso de tareas
    • Ayúdame a definir el contrato TypeScript entre frontend y backend
    • Revisa si mi API maneja bien la Idempotency-Key
    • Cómo debo estructurar los errores de mi API

    SKILL.md

    En inglés

    API and Interface Design

    Overview

    Design stable, well-documented interfaces that are hard to misuse. Good interfaces make the right thing easy and the wrong thing hard. This applies to REST APIs, GraphQL schemas, module boundaries, component props, and any surface where one piece of code talks to another.

    When to Use

    • Designing new API endpoints
    • Defining module boundaries or contracts between teams
    • Creating component prop interfaces
    • Establishing database schema that informs API shape
    • Changing existing public interfaces

    Core Principles

    Hyrum's Law

    With a sufficient number of users of an API, all observable behaviors of your system will be depended on by somebody, regardless of what you promise in the contract.

    This means: every public behavior — including undocumented quirks, error message text, timing, and ordering — becomes a de facto contract once users depend on it. Design implications:

    • Be intentional about what you expose. Every observable behavior is a potential commitment.
    • Don't leak implementation details. If users can observe it, they will depend on it.
    • Plan for deprecation at design time. See deprecation-and-migration for how to safely remove things users depend on.
    • Tests are not enough. Even with perfect contract tests, Hyrum's Law means "safe" changes can break real users who depend on undocumented behavior.

    The One-Version Rule

    Avoid forcing consumers to choose between multiple versions of the same dependency or API. Diamond dependency problems arise when different consumers need different versions of the same thing. Design for a world where only one version exists at a time — extend rather than fork.

    1. Contract First

    Define the interface before implementing it. The contract is the spec — implementation follows.

    // Define the contract first
    interface TaskAPI {
      // Creates a task and returns the created task with server-generated fields
      createTask(input: CreateTaskInput): Promise<Task>;
    
      // Returns paginated tasks matching filters
      listTasks(params: ListTasksParams): Promise<PaginatedResult<Task>>;
    
      // Returns a single task or throws NotFoundError
      getTask(id: string): Promise<Task>;
    
      // Partial update — only provided fields change
      updateTask(id: string, input: UpdateTaskInput): Promise<Task>;
    
      // Idempotent delete — succeeds even if already deleted
      deleteTask(id: string): Promise<void>;
    }
    

    2. Consistent Error Semantics

    Pick one error strategy and use it everywhere:

    // REST: HTTP status codes + structured error body
    // Every error response follows the same shape
    interface APIError {
      error: {
        code: string;        // Machine-readable: "VALIDATION_ERROR"
        message: string;     // Human-readable: "Email is required"
        details?: unknown;   // Additional context when helpful
      };
    }
    
    // Status code mapping
    // 400 → Client sent invalid data
    // 401 → Not authenticated
    // 403 → Authenticated but not authorized
    // 404 → Resource not found
    // 409 → Conflict (duplicate, version mismatch)
    // 422 → Validation failed (semantically invalid)
    // 500 → Server error (never expose internal details)
    

    Don't mix patterns. If some endpoints throw, others return null, and others return { error } — the consumer can't predict behavior.

    3. Validate at Boundaries

    Trust internal code. Validate at system edges where external input enters:

    // Validate at the API boundary
    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(),
          },
        });
      }
    
      // After validation, internal code trusts the types
      const task = await taskService.create(result.data);
      return res.status(201).json(task);
    });
    

    Where validation belongs:

    • API route handlers (user input)
    • Form submission handlers (user input)
    • External service response parsing (third-party data -- always treat as untrusted)
    • Environment variable loading (configuration)

    Third-party API responses are untrusted data. Validate their shape and content before using them in any logic, rendering, or decision-making. A compromised or misbehaving external service can return unexpected types, malicious content, or instruction-like text.

    Where validation does NOT belong:

    • Between internal functions that share type contracts
    • In utility functions called by already-validated code
    • On data that just came from your own database

    4. Prefer Addition Over Modification

    Extend interfaces without breaking existing consumers:

    // Good: Add optional fields
    interface CreateTaskInput {
      title: string;
      description?: string;
      priority?: 'low' | 'medium' | 'high';  // Added later, optional
      labels?: string[];                       // Added later, optional
    }
    
    // Bad: Change existing field types or remove fields
    interface CreateTaskInput {
      title: string;
      // description: string;  // Removed — breaks existing consumers
      priority: number;         // Changed from string — breaks existing consumers
    }
    

    5. Predictable Naming

    Pattern Convention Example
    REST endpoints Plural nouns, no verbs GET /api/tasks, POST /api/tasks
    Query params camelCase ?sortBy=createdAt&pageSize=20
    Response fields camelCase { createdAt, updatedAt, taskId }
    Boolean fields is/has/can prefix isComplete, hasAttachments
    Enum values UPPER_SNAKE "IN_PROGRESS", "COMPLETED"

    6. Honouring an Idempotency Key

    Accepting an Idempotency-Key is the contract. Honouring it is the implementation, and it is where the money is lost — a key the server accepts but handles carelessly is worse than no key at all, because the client now believes retrying is safe.

    Derive the key from the intent, not the attempt. The key must be stable across retries of one intent and different across distinct intents:

    crypto.randomUUID()                    // ✗ new key per attempt — every retry is a new charge
    `${userId}:${amount}`                  // ✗ two legitimate $50 charges collapse into one
    `${orderId}:${Date.now()}`             // ✗ a timestamp is randomUUID() wearing a hat
    
    req.headers['idempotency-key']         // ✓ client generates once, reuses on retry
    `charge:v1:${orderId}`                 // ✓ derived from an immutable identifier
    

    The key comes from the client or the initiating event — never from the layer doing the retrying.

    Claim atomically. A check followed by an act is a race:

    // ✗ TOCTOU: two concurrent retries both read "not seen", both charge
    if (!(await db.exists(key))) {
      await chargeCard(amount);
      await db.insert(key);
    }
    
    // ✓ let the unique constraint pick the winner
    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 });
    

    The unique constraint is the mechanism. A store that cannot enforce uniqueness in one operation cannot back this.

    Guard the payload. Same key with a different body is a client bug, and must fail loudly rather than serving the first response to a second request:

    if (existing.requestHash !== hash(req.body)) {
      return res.status(422).json({ error: 'idempotency key reused with a different payload' });
    }
    

    Decide what an in-flight duplicate gets. The first request is still running when the second arrives — the common case under retry storms:

    Strategy Response Use when
    Reject 409 Conflict Client can retry later; simplest and safest
    Wait Block for the result, bounded Caller needs it synchronously
    Return pending 202 + status URL Long-running effects

    Never let the second caller through because the first "seems stuck". A stalled attempt whose fate is unknown is exactly when duplicating costs most.

    Every call has three outcomes, not two: success, failure, and unknown. A timeout tells you nothing about whether the effect applied. Record the intent before calling out, so a crash between the call and the response leaves evidence something must resolve later — rather than a silently retried charge.

    Set retention from the longest retry chain, not from disk cost. Keys must outlive every path that can re-deliver the same intent, including a dead-letter queue replayed a week later and any provider dispute window. A 24-hour key TTL behind a 7-day DLQ is a duplicate waiting to happen.

    REST API Patterns

    Resource Design

    GET    /api/tasks              → List tasks (with query params for filtering)
    POST   /api/tasks              → Create a task
    GET    /api/tasks/:id          → Get a single task
    PATCH  /api/tasks/:id          → Update a task (partial)
    DELETE /api/tasks/:id          → Delete a task
    
    GET    /api/tasks/:id/comments → List comments for a task (sub-resource)
    POST   /api/tasks/:id/comments → Add a comment to a task
    

    Pagination

    Paginate list endpoints:

    // Request
    GET /api/tasks?page=1&pageSize=20&sortBy=createdAt&sortOrder=desc
    
    // Response
    {
      "data": [...],
      "pagination": {
        "page": 1,
        "pageSize": 20,
        "totalItems": 142,
        "totalPages": 8
      }
    }
    

    Filtering

    Use query parameters for filters:

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

    Partial Updates (PATCH)

    Accept partial objects — only update what's provided:

    // Only title changes, everything else preserved
    PATCH /api/tasks/123
    { "title": "Updated title" }
    

    TypeScript Interface Patterns

    Use Discriminated Unions for Variants

    // Good: Each variant is explicit
    type TaskStatus =
      | { type: 'pending' }
      | { type: 'in_progress'; assignee: string; startedAt: Date }
      | { type: 'completed'; completedAt: Date; completedBy: string }
      | { type: 'cancelled'; reason: string; cancelledAt: Date };
    
    // Consumer gets 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}`;
      }
    }
    

    Input/Output Separation

    // Input: what the caller provides
    interface CreateTaskInput {
      title: string;
      description?: string;
    }
    
    // Output: what the system returns (includes server-generated fields)
    interface Task {
      id: string;
      title: string;
      description: string | null;
      createdAt: Date;
      updatedAt: Date;
      createdBy: string;
    }
    

    Use Branded Types for IDs

    type TaskId = string & { readonly __brand: 'TaskId' };
    type UserId = string & { readonly __brand: 'UserId' };
    
    // Prevents accidentally passing a UserId where a TaskId is expected
    function getTask(id: TaskId): Promise<Task> { ... }
    

    Common Rationalizations

    Rationalization Reality
    "We'll document the API later" The types ARE the documentation. Define them first.
    "We don't need pagination for now" You will the moment someone has 100+ items. Add it from the start.
    "PATCH is complicated, let's just use PUT" PUT requires the full object every time. PATCH is what clients actually want.
    "We'll version the API when we need to" Breaking changes without versioning break consumers. Design for extension from the start.
    "Nobody uses that undocumented behavior" Hyrum's Law: if it's observable, somebody depends on it. Treat every public behavior as a commitment.
    "We can just maintain two versions" Multiple versions multiply maintenance cost and create diamond dependency problems. Prefer the One-Version Rule.
    "Internal APIs don't need contracts" Internal consumers are still consumers. Contracts prevent coupling and enable parallel work.
    "Accepting the Idempotency-Key header is enough" The header is the contract; storing the key against the result is the implementation. A key you accept but don't honour tells the client retrying is safe when it isn't.
    "Our queue guarantees exactly-once delivery" No queue does across a consumer crash — the broker's ack and your side effect are not in one transaction. Design for at-least-once with idempotent processing.
    "Duplicate requests are rare" They're correlated. Retries spike exactly when a dependency is degraded — the moment duplicates are most likely and most expensive.

    Red Flags

    • Endpoints that return different shapes depending on conditions
    • Inconsistent error formats across endpoints
    • Validation scattered throughout internal code instead of at boundaries
    • Breaking changes to existing fields (type changes, removals)
    • List endpoints without pagination
    • Verbs in REST URLs (/api/createTask, /api/getUsers)
    • Third-party API responses used without validation or sanitization
    • A SELECT for an idempotency key followed by an INSERT — that's a race, not a guard
    • An idempotency key derived from a UUID, timestamp, or anything else regenerated per attempt
    • The same key accepted with a different request body, silently returning the first response
    • A key retention window shorter than the longest path that can re-deliver the request

    Verification

    After designing an API:

    • Every endpoint has typed input and output schemas
    • Error responses follow a single consistent format
    • Validation happens at system boundaries only
    • List endpoints support pagination
    • New fields are additive and optional (backward compatible)
    • Naming follows consistent conventions across all endpoints
    • API documentation or types are committed alongside the implementation
    • State-changing endpoints either honour an idempotency key or are documented as unsafe to retry
    • The key is claimed in one atomic operation, guarded by a unique constraint
    • A reused key with a different payload fails loudly rather than replaying the wrong response
    • The in-flight-duplicate response is a deliberate choice (409, wait, or 202) rather than whatever falls out
    • Key retention outlives the longest retry path, including dead-letter replay

    Reproducido de addyosmani/agent-skills bajo licencia MIT. Leer esta página en markdown.

    Archivos

    1 archivo en el paquete. Solo se lee SKILL.md al activarse — las referencias se cargan si el skill decide que las necesita.

    Detalles

    Creador
    addyosmani
    Licencia
    MIT
    Recursos incluidos
    Solo SKILL.md
    Código fuente
    Ver SKILL.md

    Etiquetas

    Más de addyosmani/agent-skills

    Este repo incluye 24 skills. Si instalas uno, normalmente ya tienes los demás.

    Endurece el código contra vulnerabilidades. Úsalo al manejar entrada de usuario, autenticación, almacenamiento de datos, integraciones externas o datos personales (GDPR, CCPA).

    Costo de contexto al activarse
    6k tok
    Tamaño del paquete
    1 archivo
    Última actualización
    anteayer
    seguridad

    Crea especificaciones antes de programar: úsalo al iniciar un proyecto o cambio sin spec, cuando los requisitos son ambiguos, o cuando un requerimiento debe descomponerse en un mapa de módulos.

    Costo de contexto al activarse
    3k tok
    Tamaño del paquete
    1 archivo
    Última actualización
    anteayer
    herramientas desarrollo

    Realiza revisión de código en múltiples ejes. Úsalo antes de fusionar cualquier cambio, sea escrito por ti, otro agente o una persona, para evaluar la calidad antes de entrar a la rama principal.

    Costo de contexto al activarse
    5.1k tok
    Tamaño del paquete
    1 archivo
    Última actualización
    hace 9 días
    testing qa

    Divide el trabajo en tareas ordenadas. Úsalo cuando tengas un spec o requisitos claros y necesites descomponer el trabajo en tareas implementables, estimar alcance o paralelizar.

    Costo de contexto al activarse
    2.4k tok
    Tamaño del paquete
    1 archivo
    Última actualización
    hace 4 días
    productividad

    Instrumenta el código para que el comportamiento en producción sea visible y diagnosticable, con logging, métricas, tracing y alertas.

    Costo de contexto al activarse
    2.8k tok
    Tamaño del paquete
    1 archivo
    Última actualización
    hace 9 días
    devops infraestructura

    Descubre e invoca las skills de agente adecuadas; es la meta-skill que gobierna cómo se descubren y aplican todas las demás skills según la fase de desarrollo.

    Costo de contexto al activarse
    2.6k tok
    Tamaño del paquete
    1 archivo
    Última actualización
    hace 9 días
    productividad

    Skills relacionados

    Prueba en navegadores reales usando Chrome DevTools MCP. Úsalo al construir o depurar cualquier cosa que corra en un navegador, o para inspeccionar DOM, consola, red o rendimiento.

    Costo de contexto al activarse
    3.6k tok
    Tamaño del paquete
    1 archivo
    Última actualización
    hace 2 meses
    testing qa

    Automatiza la configuración de pipelines de CI/CD: úsalo al montar o modificar pipelines de build y despliegue, o al configurar puertas de calidad y test runners en CI.

    Costo de contexto al activarse
    2.8k tok
    Tamaño del paquete
    1 archivo
    Última actualización
    hace 4 meses
    devops infraestructura

    Realiza revisión de código en múltiples ejes. Úsalo antes de fusionar cualquier cambio, sea escrito por ti, otro agente o una persona, para evaluar la calidad antes de entrar a la rama principal.

    Costo de contexto al activarse
    5.1k tok
    Tamaño del paquete
    1 archivo
    Última actualización
    hace 9 días
    testing qa