Reglas de la API
Las reglas de la API HTTP mantienen los backends de Node predecibles para clientes, BFFs e integraciones de socios entre servicios.
Receta
Tarjeta de receta de referencia rápida - lista para copiar y pegar.
app.get("/invoices", async (req, res) => {
const limit = Math.min(Number(req.query.limit ?? 20), 100);
const cursor = req.query.cursor as string | undefined;
const page = await listInvoices({ limit, cursor });
res.json({ data: page.items, nextCursor: page.nextCursor });
});
app.post("/invoices", async (req, res) => {
const key = req.headers["idempotency-key"];
if (!key) return res.status(400).json({ error: { code: "missing_idempotency_key", message: "required" } });
// ...
});Cuándo usarlo:
- APIs HTTP REST o JSON públicas consumidas por web/móvil/socios.
- Múltiples equipos implementan servicios que los clientes tratan de manera uniforme.
- Las mutaciones de pago, pedido o facturación necesitan reintentos seguros.
Ejemplo de trabajo
// src/errors/http-error.ts
export class HttpError extends Error {
constructor(
public status: number,
public code: string,
message: string,
) {
super(message);
}
}
export function errorHandler(err: unknown, _req: express.Request, res: express.Response, _next: express.NextFunction) {
if (err instanceof HttpError) {
return res.status(err.status).json({ error: { code: err.code, message: err.message } });
}
res.status(500).json({ error: { code: "internal_error", message: "unexpected error" } });
}// src/routes/invoices.ts
app.get("/v1/invoices", async (req, res, next) => {
try {
const limit = Math.min(Math.max(Number(req.query.limit ?? 20), 1), 100);
const result = await repo.list({ limit, cursor: req.query.cursor as string | undefined });
res.json({ data: result.rows, nextCursor: result.nextCursor });
} catch (e) {
next(e);
}
});
app.post("/v1/invoices", async (req, res, next) => {
try {
const idempotencyKey = req.headers["idempotency-key"];
if (typeof idempotencyKey !== "string") {
throw new HttpError(400, "missing_idempotency_key", "Se requiere el encabezado Idempotency-Key");
}
const existing = await repo.findByIdempotencyKey(idempotencyKey);
if (existing) return res.status(200).json({ data: existing });
const created = await repo.create({ ...req.body, idempotencyKey });
res.status(201).json({ data: created });
} catch (e) {
next(e);
}
});Lo que esto demuestra:
- Envoltura de éxito uniforme
{ error: { code, message } }y{ data: ... }. - Paginación con
limitynextCursorlimitados. - POST idempotente devuelve 200 en reintentos, 201 en la primera creación.
Análisis Profundo
Cómo funciona
- El prefijo de versión
/v1/permite cambios importantes en/v2/sin interrupciones silenciosas del cliente. - El
codede error es legible por máquina; elmessagees seguro para humanos (sin rastros de pila). - Las claves de idempotencia almacenadas con un índice único evitan cargos duplicados.
409 Conflictpara conflictos de estado (factura ya cancelada).
Guía de códigos de estado
| Código | Uso |
|---|---|
| 400 | Falló la validación |
| 401 | Autenticación faltante/inválida |
| 403 | Autenticación correcta, no permitido |
| 404 | Recurso no encontrado |
| 409 | Conflicto / duplicado |
| 422 | Validación semántica (opcional) |
| 429 | Límite de tasa excedido |
| 500 | Error inesperado del servidor |
Notas de TypeScript
- Comparte tipos de respuesta en
packages/contractspara consumidores de monorepos. - Los errores de análisis de Zod se asignan a 400 con
code: "validation_error".
Errores comunes
- Diferentes formas de error por ruta - Los clientes no pueden analizar de forma fiable. Solución: solo middleware de error global.
limit=999999ilimitado - DB OOM. Solución: límite máximo de 100 en el servidor siempre.- Idempotencia solo en la documentación - Los reintentos duplican el cargo. Solución: restricción única de DB en la clave.
- Devolver 200 con cuerpo de error - Rompe la semántica HTTP. Solución: códigos de estado adecuados.
- Filtrar IDs internos en errores -
postgres uuid constrainten el mensaje. Solución: mensaje genérico, detalles solo en los registros.
Alternativas
| Alternativa | Cuándo usar | Cuándo NO usar |
|---|---|---|
| GraphQL | Consultas de cliente flexibles | APIs de socios CRUD simples |
| gRPC interno | Rendimiento de servicio a servicio | API pública orientada al navegador |
| Problemas Detalles RFC 7807 | Organizaciones con muchos estándares | Clientes existentes con { error: { code } } |
Preguntas frecuentes
¿Paginación por cursor vs. por desplazamiento?
Prefiere el cursor para tablas grandes (estable bajo inserciones). El desplazamiento está bien para listas de bajo volumen de administración.
¿Idempotency-Key en PUT/PATCH?
Requerido en las creaciones POST; PUT a menudo es naturalmente idempotente por ID de recurso; documentar por endpoint.
¿El envoltorio de éxito siempre debe usar datos?
Elige un envoltorio para toda la organización; { data } es común para la consistencia tipo JSON:API.
¿Errores de validación de esquema de Fastify?
Mapea la validación de Fastify a la misma forma JSON de error { error: { code, message } } en setErrorHandler.
¿Filtros de excepción de NestJS?
Un filtro personalizado mapea HttpException a la forma JSON de error compartida.
¿Cuerpo de respuesta de límite de tasa?
429 con encabezado Retry-After y error.code: "rate_limited".
¿Idempotencia de DELETE?
Eliminar dos veces devuelve 204 o 404 consistentemente; documenta cuál.
¿Fechas ISO8601 en JSON?
Sí, cadenas en UTC 2026-07-09T12:00:00.000Z; nunca ambiguas locales sin desplazamiento.
¿OpenAPI requerido?
Recomendado para APIs públicas; genera a partir de Zod/esquemas cuando sea posible.
¿Versión en el encabezado vs. en la ruta?
La ruta /v1 es más clara para el almacenamiento en caché y el enrutamiento; el encabezado es un suplemento opcional.
Relacionado
- Lista de verificación de reglas del proyecto Node - reglas 11-13
- Reglas de seguridad - validación en el límite
- Reglas de registro y observabilidad - registra el ID de solicitud con errores
Versiones de la pila: Esta página fue escrita para Node.js 24.18.0 (LTS activa), npm 10+, TypeScript 5.6+, Express 5, Fastify 5 y NestJS 11.