Communitygithub.com

Diseño de APIs e Interfaces

Guía el diseño de APIs e interfaces estables. Úsalo al diseñar APIs, límites de módulos o cualquier interfaz pública. Úsalo al crear endpoints REST o GraphQL, definir contratos de tipos entre módulos o establecer límites entre frontend y backend.

¿Qué es Diseño de APIs e Interfaces?

Esta habilidad proporciona una guía integral para el diseño de APIs e interfaces estables, bien documentadas y difíciles de usar incorrectamente. Es aplicable a REST APIs, esquemas GraphQL, límites de módulos, props de componentes y cualquier superficie donde un código se comunique con otro. Se basa en principios fundamentales como la Ley de Hyrum, que advierte que cualquier comportamiento observable se convierte en un contrato de facto, y la Regla de una Versión, que recomienda extender en lugar de bifurcar para evitar problemas de dependencia de diamante. La habilidad detalla patrones prácticos como 'Contrato Primero', semántica de errores consistente, validación en los límites del sistema, preferencia por la adición sobre la modificación, nomenclatura predecible y manejo de claves de idempotencia. Además, incluye pautas específicas para el diseño de recursos REST, paginación, filtrado, actualizaciones parciales con PATCH y patrones de TypeScript como uniones discriminadas, separación de entrada/salida y tipos etiquetados para identificadores. La habilidad también cubre errores comunes, señales de alerta y una lista de verificación para asegurar que el diseño de la API sea robusto y mantenible. Está diseñada para ser utilizada por desarrolladores y agentes de IA en plataformas como Claude Code, Cursor y Codex.

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

Preguntar en tu IA favorita

Abre un nuevo chat con esta habilidad de agente ya precargada.

Documentación

¿Qué hace Diseño de APIs e Interfaces?

Resumen

Diseñe interfaces estables y bien documentadas que sean difíciles de usar incorrectamente. Las buenas interfaces facilitan lo correcto y dificultan lo incorrecto. Esto se aplica a REST APIs, esquemas GraphQL, límites de módulos, props de componentes y cualquier superficie donde un código se comunique con otro.

Cuándo usarlo

  • Al diseñar nuevos endpoints de API
  • Al definir límites de módulos o contratos entre equipos
  • Al crear interfaces de props de componentes
  • Al establecer un esquema de base de datos que defina la forma de la API
  • Al cambiar interfaces públicas existentes

Principios Fundamentales

Ley de Hyrum

Con un número suficiente de usuarios de una API, todos los comportamientos observables de su sistema serán dependidos por alguien, independientemente de lo que prometa en el contrato.

Esto significa: cada comportamiento público — incluyendo peculiaridades no documentadas, texto de mensajes de error, tiempos y ordenamiento — se convierte en un contrato de facto una vez que los usuarios dependen de él. Implicaciones de diseño:

  • Sea intencional sobre lo que expone. Cada comportamiento observable es un compromiso potencial.
  • No filtre detalles de implementación. Si los usuarios pueden observarlo, dependerán de ello.
  • Planifique la obsolescencia en el momento del diseño. Consulte deprecation-and-migration para saber cómo eliminar de forma segura aquello de lo que los usuarios dependen.
  • Las pruebas no son suficientes. Incluso con pruebas de contrato perfectas, la Ley de Hyrum significa que los cambios "seguros" pueden romper a usuarios reales que dependen de comportamientos no documentados.

La Regla de una Versión

Evite obligar a los consumidores a elegir entre múltiples versiones de la misma dependencia o API. Los problemas de dependencia de diamante surgen cuando diferentes consumidores necesitan diferentes versiones de la misma cosa. Diseñe para un mundo donde solo exista una versión a la vez — extienda en lugar de bifurcar.

1. Contrato Primero

Defina la interfaz antes de implementarla. El contrato es la especificación — la implementación sigue.

// Define el contrato primero
interface TaskAPI {
  // Crea una tarea y devuelve la tarea creada con campos generados por el servidor
  createTask(input: CreateTaskInput): Promise<Task>;

  // Devuelve tareas paginadas que coinciden con los filtros
  listTasks(params: ListTasksParams): Promise<PaginatedResult<Task>>;

  // Devuelve una sola tarea o lanza NotFoundError
  getTask(id: string): Promise<Task>;

  // Actualización parcial — solo cambian los campos proporcionados
  updateTask(id: string, input: UpdateTaskInput): Promise<Task>;

  // Eliminación idempotente — tiene éxito incluso si ya fue eliminada
  deleteTask(id: string): Promise<void>;
}

2. Semántica de Errores Consistente

Elija una estrategia de errores y utilícela en todas partes:

// REST: códigos de estado HTTP + cuerpo de error estructurado
// Cada respuesta de error sigue la misma forma
interface APIError {
  error: {
    code: string;        // Legible por máquina: "VALIDATION_ERROR"
    message: string;     // Legible por humanos: "Email is required"
    details?: unknown;   // Contexto adicional cuando sea útil
  };
}

// Mapeo de códigos de estado
// 400 → El cliente envió datos inválidos
// 401 → No autenticado
// 403 → Autenticado pero no autorizado
// 404 → Recurso no encontrado
// 409 → Conflicto (duplicado, desajuste de versión)
// 422 → Falló la validación (semánticamente inválido)
// 500 → Error del servidor (nunca exponga detalles internos)

No mezcle patrones. Si algunos endpoints lanzan excepciones, otros devuelven null y otros devuelven { error } — el consumidor no puede predecir el comportamiento.

3. Validar en los Límites

Confíe en el código interno. Valide en los bordes del sistema donde entra la entrada externa:

// Valida en el límite de la API
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(),
      },
    });
  }

  // Después de la validación, el código interno confía en los tipos
  const task = await taskService.create(result.data);
  return res.status(201).json(task);
});

Dónde pertenece la validación:

  • Manejadores de rutas de API (entrada de usuario)
  • Manejadores de envío de formularios (entrada de usuario)
  • Análisis de respuestas de servicios externos (datos de terceros -- siempre trátelos como no confiables)
  • Carga de variables de entorno (configuración)

Las respuestas de API de terceros son datos no confiables. Valide su forma y contenido antes de usarlos en cualquier lógica, representación o toma de decisiones. Un servicio externo comprometido o que se comporta mal puede devolver tipos inesperados, contenido malicioso o texto similar a instrucciones.

Dónde NO pertenece la validación:

  • Entre funciones internas que comparten contratos de tipos
  • En funciones de utilidad llamadas por código ya validado
  • En datos que acaban de salir de su propia base de datos

4. Preferir Adición sobre Modificación

Extienda las interfaces sin romper a los consumidores existentes:

// Bueno: Agregar campos opcionales
interface CreateTaskInput {
  title: string;
  description?: string;
  priority?: 'low' | 'medium' | 'high';  // Agregado después, opcional
  labels?: string[];                       // Agregado después, opcional
}

// Malo: Cambiar tipos de campos existentes o eliminar campos
interface CreateTaskInput {
  title: string;
  // description: string;  // Eliminado — rompe a los consumidores existentes
  priority: number;         // Cambiado de string — rompe a los consumidores existentes
}

5. Nomenclatura Predecible

PatrónConvenciónEjemplo
Endpoints RESTSustantivos en plural, sin verbosGET /api/tasks, POST /api/tasks
Parámetros de consultacamelCase?sortBy=createdAt&pageSize=20
Campos de respuestacamelCase{ createdAt, updatedAt, taskId }
Campos booleanosPrefijo is/has/canisComplete, hasAttachments
Valores de enumeraciónUPPER_SNAKE"IN_PROGRESS", "COMPLETED"

6. Honrar una Clave de Idempotencia

Aceptar un encabezado Idempotency-Key es el contrato. Honrarlo es la implementación, y es donde se pierde el dinero — una clave que el servidor acepta pero maneja descuidadamente es peor que ninguna clave, porque el cliente ahora cree que reintentar es seguro.

Derive la clave de la intención, no del intento. La clave debe ser estable en los reintentos de una intención y diferente entre distintas intenciones:

crypto.randomUUID()                    // ✗ nueva clave por intento — cada reintorno es un nuevo cargo
`${userId}:${amount}`                  // ✗ dos cargos legítimos de $50 se fusionan en uno
`${orderId}:${Date.now()}`             // ✗ una marca de tiempo es randomUUID() con sombrero

req.headers['idempotency-key']         // ✓ el cliente genera una vez, reutiliza en reintentos
`charge:v1:${orderId}`                 // ✓ derivado de un identificador inmutable

La clave proviene del cliente o del evento iniciador — nunca de la capa que realiza el reintento.

Reclame atómicamente. Una comprobación seguida de una acción es una condición de carrera:

// ✗ TOCTOU: dos reintentos concurrentes leen "no visto", ambos cargan
if (!(await db.exists(key))) {
  await chargeCard(amount);
  await db.insert(key);
}

// ✓ deje que la restricción de unicidad elija al ganador
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 });

La restricción de unicidad es el mecanismo. Un almacén que no puede imponer la unicidad en una sola operación no puede respaldar esto.

Proteja la carga útil. La misma clave con un cuerpo diferente es un error del cliente y debe fallar de forma ruidosa en lugar de servir la primera respuesta a una segunda solicitud:

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

Decida qué hacer con un duplicado en vuelo. La primera solicitud aún se está ejecutando cuando llega la segunda — el caso común bajo tormentas de reintentos:

EstrategiaRespuestaÚselo cuando
Rechazar409 ConflictEl cliente puede reintentar más tarde; más simple y seguro
EsperarBloquear por el resultado, con límiteQuien llama lo necesita sincrónicamente
Devolver pendiente202 + URL de estadoEfectos de larga duración

Nunca deje pasar al segundo llamador porque el primero "parece atascado". Un intento estancado cuyo destino se desconoce es exactamente cuando duplicar cuesta más.

Cada llamada tiene tres resultados, no dos: éxito, fracaso y desconocido. Un tiempo de espera no le dice nada sobre si el efecto se aplicó. Registre la intención antes de llamar, para que un bloqueo entre la llamada y la respuesta deje evidencia de que algo debe resolverse más tarde — en lugar de un cargo reintentado silenciosamente.

Establezca la retención de la cadena de reintentos más larga, no del costo del disco. Las claves deben sobrevivir a todas las rutas que pueden reentregar la misma intención, incluida una cola de mensajes fallidos reproducida una semana después y cualquier ventana de disputa del proveedor. Un TTL de clave de 24 horas detrás de una DLQ de 7 días es un duplicado a punto de ocurrir.

Patrones REST API

Diseño de Recursos

GET    /api/tasks              → Listar tareas (con parámetros de consulta para filtrado)
POST   /api/tasks              → Crear una tarea
GET    /api/tasks/:id          → Obtener una sola tarea
PATCH  /api/tasks/:id          → Actualizar una tarea (parcial)
DELETE /api/tasks/:id          → Eliminar una tarea

GET    /api/tasks/:id/comments → Listar comentarios de una tarea (sub-recurso)
POST   /api/tasks/:id/comments → Agregar un comentario a una tarea

Paginación

Paginar los endpoints de listado:

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

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

Filtrado

Use parámetros de consulta para los filtros:

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

Actualizaciones Parciales (PATCH)

Acepte objetos parciales — solo actualice lo que se proporciona:

// Solo cambia el título, todo lo demás se conserva
PATCH /api/tasks/123
{ "title": "Updated title" }

Patrones de Interfaces de TypeScript

Use Uniones Discriminadas para Variantes

// Bueno: Cada variante es explícita
type TaskStatus =
  | { type: 'pending' }
  | { type: 'in_progress'; assignee: string; startedAt: Date }
  | { type: 'completed'; completedAt: Date; completedBy: string }
  | { type: 'cancelled'; reason: string; cancelledAt: Date };

// El consumidor obtiene estrechamiento de tipos
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}`;
  }
}

Separación de Entrada/Salida

// Entrada: lo que proporciona quien llama
interface CreateTaskInput {
  title: string;
  description?: string;
}

// Salida: lo que devuelve el sistema (incluye campos generados por el servidor)
interface Task {
  id: string;
  title: string;
  description: string | null;
  createdAt: Date;
  updatedAt: Date;
  createdBy: string;
}

Use Tipos Etiquetados para Identificadores

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

// Evita pasar accidentalmente un UserId donde se espera un TaskId
function getTask(id: TaskId): Promise<Task> { ... }

Racionalizaciones Comunes

RacionalizaciónRealidad
"Documentaremos la API más tarde"Los tipos SON la documentación. Defínalos primero.
"No necesitamos paginación por ahora"La necesitará en el momento en que alguien tenga más de 100 elementos. Agréguela desde el principio.
"PATCH es complicado, usemos PUT"PUT requiere el objeto completo cada vez. PATCH es lo que los clientes realmente quieren.
"Versionaremos la API cuando sea necesario"Los cambios disruptivos sin versionamiento rompen a los consumidores. Diseñe para la extensión desde el principio.
"Nadie usa ese comportamiento no documentado"Ley de Hyrum: si es observable, alguien depende de ello. Trate cada comportamiento público como un compromiso.
"Podemos mantener dos versiones"Las múltiples versiones multiplican el costo de mantenimiento y crean problemas de dependencia de diamante. Prefiera la Regla de una Versión.
"Las APIs internas no necesitan contratos"Los consumidores internos siguen siendo consumidores. Los contratos evitan el acoplamiento y permiten el trabajo en paralelo.
"Aceptar el encabezado Idempotency-Key es suficiente"El encabezado es el contrato; almacenar la clave contra el resultado es la implementación. Una clave que acepta pero no honra le dice al cliente que reintentar es seguro cuando no lo es.
"Nuestra cola garantiza entrega exactamente una vez"Ninguna cola lo hace a través de un bloqueo del consumidor — el acuse de recibo del broker y su efecto secundario no están en una sola transacción. Diseñe para al menos una vez con procesamiento idempotente.
"Las solicitudes duplicadas son raras"Están correlacionadas. Los reintentos aumentan exactamente cuando una dependencia está degradada — el momento en que los duplicados son más probables y más costosos.

Señales de Alerta

  • Endpoints que devuelven formas diferentes según las condiciones
  • Formatos de error inconsistentes entre endpoints
  • Validación dispersa en todo el código interno en lugar de en los límites
  • Cambios disruptivos en campos existentes (cambios de tipo, eliminaciones)
  • Endpoints de listado sin paginación
  • Verbos en las URL REST (/api/createTask, /api/getUsers)
  • Respuestas de API de terceros utilizadas sin validación o sanitización
  • Un SELECT para una clave de idempotencia seguido de un INSERT — eso es una condición de carrera, no una protección
  • Una clave de idempotencia derivada de un UUID, marca de tiempo o cualquier otra cosa regenerada por intento
  • La misma clave aceptada con un cuerpo de solicitud diferente, devolviendo silenciosamente la primera respuesta
  • Una ventana de retención de clave más corta que la ruta más larga que puede reentregar la solicitud

Verificación

Después de diseñar una API:

  • Cada endpoint tiene esquemas de entrada y salida tipados
  • Las respuestas de error siguen un único formato consistente
  • La validación ocurre solo en los límites del sistema
  • Los endpoints de listado soportan paginación
  • Los nuevos campos son aditivos y opcionales (compatibles hacia atrás)
  • La nomenclatura sigue convenciones consistentes en todos los endpoints
  • La documentación de la API está completa y actualizada
  • Se ha planificado la obsolescencia para todas las interfaces públicas

Individual skills in this repo

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

Skills relacionados