Next.js
Diseño de APIs dentro de Next.js
Explica cómo diseñar APIs consistentes en Next.js con recursos, métodos, DTOs, errores, paginación, versionado, idempotencia y límites operativos.
- Última actualización
- Actualizada
- Nivel
- Aplicación
Next.js
Explica cómo diseñar APIs consistentes en Next.js con recursos, métodos, DTOs, errores, paginación, versionado, idempotencia y límites operativos.
Diseñar una API dentro de Next.js significa definir un contrato estable entre consumidores y capacidades del sistema. route.ts aporta transporte HTTP; no decide recursos, autorización, versionado, errores, paginación, idempotencia ni límites operativos.
Una API completa define:
resource and URL
+ HTTP method
+ authentication
+ request schema
+ authorization
+ response schema
+ errors/status
+ pagination/filtering
+ idempotency/concurrency
+ caching/rate limits
+ versioning/observabilityEl Handler debe ser delgado y delegar en servicios o casos de uso.
Backend for Frontend adapta datos y operaciones a las necesidades de una interfaz:
browser/mobile
→ Next.js BFF
→ domain services / databases / external APIsAporta:
No significa que Next.js deba absorber jobs, colas, procesamiento pesado o todos los dominios organizacionales.
No crees microservicios solo por “escalabilidad”. Cada servicio añade red, observabilidad, autenticación y consistencia distribuida.
Preferible:
GET /api/v1/orders
POST /api/v1/orders
GET /api/v1/orders/{id}
PATCH /api/v1/orders/{id}
DELETE /api/v1/orders/{id}Evita endpoints RPC vagos:
POST /api/doOrderThingUna acción de dominio que no encaja como CRUD puede ser subrecurso:
POST /api/v1/orders/{id}/cancellationsEsto representa un evento auditable y puede tener idempotencia propia.
Seguro e idempotente; no debe mutar negocio.
Crea recurso o inicia acción; no es idempotente por defecto.
Reemplazo completo bajo una URL conocida; idempotente conceptualmente.
Cambio parcial; define formato (merge patch, JSON Patch o schema propio).
Debe ser idempotente en resultado observable: repetir no crea otro efecto.
Valida path, query, headers y body por separado:
const path = orderIdSchema.parse(orderId);
const query = listOrdersQuerySchema.parse(Object.fromEntries(url.searchParams));
const body = createOrderSchema.parse(await request.json());No uses el mismo schema de base de datos como contrato público. El API necesita versiones, campos permitidos y mensajes comprensibles.
type OrderResponse = {
id: string;
number: string;
status: "pending" | "confirmed" | "cancelled";
total: { amount: number; currency: string };
createdAt: string;
};Fechas como ISO strings, dinero con moneda y enums explícitos. No expongas nombres de columnas internos ni relaciones completas.
Formato estable:
{
"type": "https://example.com/problems/order-conflict",
"title": "Order conflict",
"status": 409,
"detail": "The order changed since it was loaded.",
"instance": "/api/v1/orders/123",
"requestId": "req_abc"
}Errores de campos pueden incluir un mapa adicional. No cambies la forma por endpoint.
?page=3&pageSize=25Sencilla, pero costosa en datasets grandes e inestable con inserciones.
?after=opaqueCursor&limit=25Más estable y eficiente si el cursor contiene orden único.
Orden:
createdAt DESC, id DESCEl ID desempata. El cursor debe ser opaco para consumidores y firmado/codificado si contiene datos sensibles.
Allowlist:
const sort = z.enum(["createdAt", "total", "status"]);Nunca uses un query param directamente como nombre de columna SQL.
Limita complejidad de filtros para evitar queries abusivas.
Opciones:
/api/v1.Para una API pública, URL versioning es explícito. Mantén una ventana de compatibilidad y política de deprecación.
Añadir campo suele ser compatible; renombrar o cambiar semántica no.
Para POST sensible:
Idempotency-Key: uuidGuarda:
Si la misma key llega con otro body, devuelve conflicto.
ETag/version:
GET → ETag: "v4"
PATCH If-Match: "v4"Si cambió:
412 Precondition FailedEvita lost updates en clientes independientes.
No mezcles mecanismos sin documentar CSRF, expiración y rotación.
actor
+ tenant
+ resource
+ action
+ current state
→ allow/denyDebe vivir cerca del caso de uso/DAL. El endpoint no confía en un role enviado por cliente.
Evita tomar organizationId del body como autoridad. Derívalo de sesión/API key y comprueba que path/body coincidan cuando el contrato lo requiera.
Queries siempre scoped. Tests con dos tenants son obligatorios.
Distingue:
Devuelve 429 y Retry-After cuando sea posible.
Los contadores necesitan almacenamiento compartido en despliegues distribuidos.
GET público:
Cache-Control: public, s-maxage=300, stale-while-revalidate=3600Privado:
Cache-Control: private, no-storeUsa Vary si la representación depende de un header permitido. Cuidado: Vary: Cookie puede destruir hit ratio y aumentar cardinalidad.
La caché interna de Next.js y HTTP cache son capas distintas.
Si tu API envía eventos:
Documenta contratos para consumidores independientes:
Puedes generar clientes y validar compatibilidad en CI.
No dejes que documentación generada de tipos internos exponga campos no deseados.
Request:
POST /api/v1/orders
Idempotency-Key: order-attempt-123
Content-Type: application/jsonBody:
{
"customerId": "cus_123",
"items": [{ "productId": "prd_1", "quantity": 2 }]
}Flujo:
auth
→ parse schema
→ authorize customer/products
→ idempotency lookup
→ transaction stock + order
→ outbox event
→ 201 Location /api/v1/orders/{id}El cliente no envía total final como autoridad; servidor calcula precio y stock.
Endpoint responde 202:
{
"jobId": "job_123",
"statusUrl": "/api/v1/jobs/job_123"
}Worker procesa. El cliente consulta o recibe webhook/SSE.
No mantengas una function serverless esperando minutos.
Puede evolucionar junto al frontend, usar cookies y DTOs de pantalla.
Necesita versionado, SLA, documentación, compatibilidad y rate limits más estrictos.
No publiques accidentalmente endpoints internos bajo un contrato del que luego no puedes escapar.
route.ts
→ parse HTTP
→ application service
→ domain rules
→ repository/integrationsEl servicio devuelve resultados de dominio; el Handler los traduce a status/DTO.
Esto permite usar el mismo caso de uso desde Action, job o CLI sin HTTP interno.
Define SLO por endpoints críticos.
Request lifecycle en Next.js ubica API, Proxy, routing, caché y rendering en el orden real de una solicitud.