Arquitectura de APIs REST: 7 prácticas para que tu backend no se caiga en producción
La mayoría de APIs fallan en producción no por falta de features, sino por decisiones de arquitectura que nadie tomó a propósito. Estas son las siete prácticas que separan una API que aguanta tu primer pico real de tráfico de una que se cae con el Black Friday — y cómo priorizarlas si no se pueden implementar todas de una vez.
1. Versiona la API desde el primer endpoint
Un prefijo como /api/v1/ cuesta nada agregar hoy y evita romper a todos tus clientes (web, móvil, integraciones) el día que necesites cambiar un contrato. El error común es pensar 'todavía no lo necesito, lo agrego cuando haga falta' — pero para cuando hace falta, ya hay clientes en producción consumiendo la versión sin prefijo, y agregar versionado retroactivamente significa mantener dos rutas para lo mismo o forzar una migración incómoda. El costo de versionar desde el día uno es prácticamente cero; el costo de agregarlo después es una migración completa de clientes.
2. Valida en el borde, no en el medio
Toda entrada externa se valida antes de tocar lógica de negocio — con una librería de esquemas (Zod, Yup), no con ifs sueltos repartidos por el código. Esto tiene un beneficio que va más allá de la seguridad: cuando la validación vive en un solo lugar, con un esquema explícito, la documentación de la API prácticamente se escribe sola, y cualquier persona nueva en el equipo puede ver exactamente qué forma espera cada endpoint sin tener que leer la implementación completa.
const OrderSchema = z.object({
items: z.array(ItemSchema).min(1),
customerId: z.string().uuid(),
});
const body = OrderSchema.parse(await req.json());3. Códigos de estado HTTP consistentes
201 al crear, 204 al borrar sin contenido, 409 en conflictos, 422 en validación fallida. Un cliente que consume tu API debería poder tomar decisiones solo con el status code, sin parsear el mensaje de error. Esto importa especialmente para integraciones automatizadas (otro sistema consumiendo tu API sin un humano leyendo la respuesta): un webhook o un job programado necesita poder decidir si reintentar, descartar o alertar basándose solo en el código, y un backend que devuelve 200 con un campo `success: false` en el cuerpo rompe esa capacidad de decisión automática.
4. Autenticación y autorización separadas
Autenticación responde '¿quién eres?'. Autorización responde '¿puedes hacer esto?'. Mezclarlas en un mismo middleware es la forma más común de terminar con un endpoint que filtra datos que no debería. El patrón más seguro es resolver la identidad una sola vez, temprano en la cadena de middleware, y luego dejar que cada endpoint (o un middleware dedicado por recurso) decida el permiso específico que necesita — nunca asumir que 'está autenticado' es lo mismo que 'puede ver esto'.
5. Rate limiting antes de que lo necesites
Un endpoint sin límite de tasa no solo es vulnerable a abuso — un solo cliente mal configurado (un cron mal hecho, un frontend con un loop) puede tumbar tu backend sin que nadie ataque nada. El rate limiting no tiene que ser sofisticado desde el inicio: un límite simple por IP o por token de API, con una respuesta 429 clara y un header `Retry-After`, ya cubre la mayoría de los casos reales. Lo importante es que exista antes del primer incidente, no después.
6. Idempotencia en operaciones críticas
Si un pago se reintenta por un timeout de red, la segunda llamada no debería cobrar dos veces. Una clave de idempotencia (Idempotency-Key) en el header resuelve esto sin lógica compleja en cada endpoint: el servidor guarda el resultado de la primera ejecución asociado a esa clave, y si llega otra request con la misma clave, devuelve el resultado guardado en vez de ejecutar la operación de nuevo. Esto es especialmente crítico en cualquier endpoint que mueva dinero, cree recursos con efectos externos (enviar un correo, disparar una notificación) o modifique inventario.
const existing = await getIdempotentResult(idempotencyKey);
if (existing) return existing;
const result = await processPayment(body);
await saveIdempotentResult(idempotencyKey, result);
return result;Un error frecuente es asumir que la idempotencia solo importa en pagos. Cualquier operación con efecto externo que no sea trivialmente reversible — enviar una notificación push, disparar un correo transaccional, crear un ticket en un sistema externo — se beneficia del mismo patrón. La regla práctica es preguntarse: si esta llamada se ejecuta dos veces por accidente, ¿el resultado visible para el usuario cambia de forma notoria? Si la respuesta es sí, ese endpoint necesita una clave de idempotencia propia, sin importar si mueve dinero o no.
7. Logging y observabilidad desde el día uno
Cuando algo falla en producción a las 2am, la pregunta no es 'qué pasó' sino '¿tengo cómo saberlo?'. Logs estructurados con un ID de correlación por request son la diferencia entre un diagnóstico de cinco minutos y una noche entera adivinando. El ID de correlación es la pieza clave: generado al inicio de cada request y propagado a través de todos los logs, llamadas a servicios externos y colas de trabajo que esa request dispare, permite reconstruir el camino completo de una petición específica sin tener que cruzar logs a mano por timestamp aproximado.
'Estructurado' es la palabra que hace la diferencia acá: un log en formato JSON con campos consistentes (timestamp, nivel, ID de correlación, mensaje, contexto adicional) se puede consultar, filtrar y agregar con herramientas — un log en texto libre solo se puede leer, línea por línea, con la esperanza de encontrar la relevante entre miles. La inversión de cambiar `console.log('error procesando pedido')` por un log estructurado con el ID del pedido y el ID de correlación es mínima al escribirlo, y enorme la primera vez que hay que investigar un incidente real bajo presión.
8. Paginación y filtrado consistentes
Un endpoint de listado sin paginación es una bomba de tiempo: funciona perfecto con diez registros en desarrollo y se cae con cien mil en producción. La convención más simple y predecible es paginación basada en cursor (un identificador del último elemento visto, no un número de página) para colecciones que crecen rápido, porque evita el problema de resultados duplicados o saltados cuando se insertan registros nuevos entre una página y la siguiente. El filtrado y el ordenamiento deberían seguir el mismo patrón de query params en todos los endpoints del API (`?status=active&sort=-createdAt`), no una convención distinta por cada recurso — la inconsistencia entre endpoints es una de las fuentes más comunes de bugs de integración en clientes que consumen la API.
9. CORS configurado a propósito, no copiado de Stack Overflow
Es común ver `Access-Control-Allow-Origin: *` en producción porque 'así dejó de dar el error en desarrollo' — y es exactamente el tipo de configuración que parece funcionar hasta que se convierte en un problema de seguridad real, sobre todo en endpoints que manejan cookies de sesión o datos sensibles. La configuración correcta declara explícitamente qué orígenes tienen permiso (el dominio del frontend real, no un comodín), qué métodos y headers están permitidos, y si se permiten credenciales — cada uno de esos valores debería ser una decisión consciente, no el primer resultado que resolvió el error en local.
Cómo priorizar si no puedes hacerlas todas de una vez
En un proyecto nuevo, el orden razonable es: versionado y validación primero (son casi gratis y evitan deuda técnica inmediata), autenticación/autorización separadas y códigos de estado consistentes segundo (afectan cómo se diseña cada endpoint desde el principio), paginación y CORS al definir los primeros endpoints de listado (retrofit después es más caro que diseñarlo bien desde el primer recurso), y rate limiting, idempotencia y observabilidad como la siguiente ola, antes del primer lanzamiento con tráfico real — no después del primer incidente que los hubiera evitado.
Un patrón que se repite en proyectos que heredamos de otro proveedor es que ninguna de estas prácticas falta por completo — lo que falta es consistencia. Un endpoint versiona y otro no, uno valida con un esquema y otro con ifs sueltos, uno pagina con cursor y otro con offset. Esa inconsistencia es, en la práctica, casi tan cara como no tener la práctica en absoluto, porque obliga a cada cliente de la API a manejar casos especiales por endpoint en vez de asumir un contrato uniforme. La disciplina de aplicar estas siete — o nueve — prácticas de forma pareja en todo el backend es tan importante como cada práctica individual.
La forma más efectiva de sostener esa consistencia en el tiempo, sobre todo cuando el equipo crece, es documentarla en un solo lugar que cualquier persona nueva pueda leer antes de escribir su primer endpoint — una guía de estilo de API interna, breve, con ejemplos reales del propio proyecto. No reemplaza la revisión de código, pero reduce drásticamente cuántas veces esa revisión termina discutiendo convenciones básicas en vez de la lógica de negocio real del cambio. Una API documentada con OpenAPI/Swagger generado a partir de los mismos esquemas de validación (en vez de mantenido a mano por separado) cumple ese mismo propósito automáticamente, sin depender de que alguien recuerde actualizar un documento aparte.
Ninguna de estas prácticas es exótica — son decisiones de arquitectura documentadas que cuestan lo mismo implementar bien desde el inicio que mal, y son exactamente lo que revisamos cuando auditamos o entregamos una API a un cliente.
