Checklist para llevar tu API a producción

Antes de publicar una API, lee la configuración de variables de entorno, expón un health check, maneja SIGTERM, estandariza los errores, autentica, limita las peticiones y escribe logs en JSON.

Actualizado el 6 min de lectura

Objetivos de aprendizaje

Después de leer este artículo podrás:

  • Revisar los puntos que separan una API de pruebas de una API en producción
  • Implementar health check, apagado limpio, errores estándar y rate limit en Node.js
  • Verificar con curl que cada punto de la checklist funciona
En esta página

Una API que funciona en tu máquina todavía no está lista para tráfico real. En producción llegan peticiones mal formadas, clientes que repiten llamadas sin parar, deploys en medio de una petición y errores que nadie ve si no hay logs. Cada punto de esta lista resuelve uno de esos problemas.

El código usa Express 5 y la versión LTS activa de Node.js, pero la checklist vale para cualquier lenguaje. Si aún no sabes dónde correrá la API, lee antes cómo alojar una API.

POST /itemsCORSsolo el origen permitidoRate limitpor IP429Autenticacióntoken Bearer401Validacióntipo y longitud400Handlertu ruta201Manejo de erroressin stack trace500Log{"method":"POST","status":201,"ms":12}
Cada capa puede cerrar la petición con el status correcto, y el log registra una línea por respuesta.

¿Qué puntos cubre la checklist?#

  1. Configuración por variables de entorno, con falla rápida si falta algo.
  2. HTTPS y un endpoint de health check.
  3. Apagado limpio al recibir SIGTERM.
  4. Errores estándar, sin filtrar detalles internos.
  5. Autenticación y validación de entrada.
  6. Rate limit y límite de tamaño del cuerpo.
  7. CORS restrictivo.
  8. Logs estructurados.
  9. Timeouts.
  10. Verificación final con curl.

Los puntos 5 a 7 cubren buena parte de los riesgos que lista el OWASP API Security Top 10 de 2023, como autenticación rota, consumo irrestricto de recursos y configuración insegura. Para el lado de la infraestructura, mira la checklist de seguridad del deploy.

¿Cómo configurar la API sin secretos en el código?#

Lee todo de variables de entorno y detén el proceso al iniciar si falta un valor obligatorio. Así un error silencioso en producción (un token undefined que acepta cualquier cosa) se convierte en una falla obvia en el primer arranque. Nunca subas secretos al repositorio. Si el concepto es nuevo para ti, lee qué son las variables de entorno.

Define también NODE_ENV=production. Con él, Express genera mensajes de error menos detallados y cachea las plantillas, algo que su documentación asocia a una mejora de rendimiento.

¿Cómo queda una API mínima con la checklist aplicada?#

Instala la dependencia y crea server.mjs:

bash
npm install express@5
js
import { timingSafeEqual } from "node:crypto";
import { STATUS_CODES } from "node:http";
import express from "express";

const { API_TOKEN, ALLOWED_ORIGIN } = process.env;
if (!API_TOKEN) {
  console.error("Falta API_TOKEN");
  process.exit(1);
}
const PORT = Number(process.env.PORT ?? 3000);

const problem = (res, status, title, detail) =>
  res.status(status).type("application/problem+json").json({ type: "about:blank", title, status, detail });

const app = express();
app.disable("x-powered-by");
app.set("trust proxy", 1);
app.use(express.json({ limit: "100kb" }));

app.use((req, res, next) => {
  const start = Date.now();
  res.on("finish", () =>
    console.log(JSON.stringify({ method: req.method, path: req.path, status: res.statusCode, ms: Date.now() - start })),
  );
  next();
});

app.use((req, res, next) => {
  if (ALLOWED_ORIGIN && req.headers.origin === ALLOWED_ORIGIN) {
    res.set("Access-Control-Allow-Origin", ALLOWED_ORIGIN);
    res.set("Access-Control-Allow-Headers", "Authorization, Content-Type");
    res.set("Vary", "Origin");
  }
  if (req.method === "OPTIONS") return res.sendStatus(204);
  next();
});

const hits = new Map();
setInterval(() => hits.clear(), 60_000).unref();
app.use((req, res, next) => {
  const n = (hits.get(req.ip) ?? 0) + 1;
  hits.set(req.ip, n);
  if (n > 100) {
    res.set("Retry-After", "60");
    return problem(res, 429, "Too Many Requests", "Límite de 100 peticiones por minuto.");
  }
  next();
});

app.get("/health", (_req, res) => res.json({ status: "ok" }));

const safeEqual = (a, b) => {
  const x = Buffer.from(a);
  const y = Buffer.from(b);
  return x.length === y.length && timingSafeEqual(x, y);
};
app.use((req, res, next) => {
  const token = (req.headers.authorization ?? "").replace(/^Bearer /, "");
  if (!safeEqual(token, API_TOKEN)) return problem(res, 401, "Unauthorized", "Token ausente o inválido.");
  next();
});

app.post("/items", async (req, res) => {
  const { name } = req.body ?? {};
  if (typeof name !== "string" || name.length === 0 || name.length > 100) {
    return problem(res, 400, "Bad Request", "name debe ser un string de 1 a 100 caracteres.");
  }
  res.status(201).json({ name });
});

app.use((err, _req, res, _next) => {
  if (err.status >= 400 && err.status < 500) return problem(res, err.status, STATUS_CODES[err.status], "Petición inválida.");
  console.error(err);
  problem(res, 500, "Internal Server Error", "Error inesperado.");
});

const server = app.listen(PORT, "0.0.0.0", () => console.log(`API en el puerto ${PORT}`));
server.requestTimeout = 30_000;

process.on("SIGTERM", () => {
  server.close(() => process.exit(0));
  setTimeout(() => {
    server.closeAllConnections();
    process.exit(1);
  }, 10_000).unref();
});

Qué hace cada bloque:

  • Config: sin API_TOKEN el proceso termina de inmediato. ALLOWED_ORIGIN es opcional.
  • trust proxy: detrás de un proxy inverso, req.ip sería la IP del proxy. Con trust proxy en 1, Express usa la IP del cliente que informa el proxy. Ajusta el valor a la cantidad de proxies delante de la API.
  • Cuerpo limitado: express.json({ limit: "100kb" }) rechaza cuerpos grandes antes de que consuman memoria.
  • Log en JSON: una línea por petición, fácil de filtrar. Nunca registres tokens, contraseñas ni el cuerpo completo.
  • CORS: solo el origen configurado recibe las cabeceras. Evita * en una API autenticada. Vary: Origin impide que una caché sirva la respuesta equivocada.
  • Rate limit: 100 peticiones por minuto por IP, en memoria. Sirve para una sola instancia; con varias, guarda los contadores en Redis.
  • Autenticación: comparación en tiempo constante con timingSafeEqual, que exige buffers del mismo tamaño (la comprobación de tamaño solo revela la longitud del token). /health va antes, para que el monitor funcione sin credencial.
  • Validación: nunca confíes en lo que envía el cliente. Valida tipo y tamaño; para esquemas mayores, usa una librería de validación.
  • Handler de errores: en Express 5, los errores lanzados en funciones async llegan solos al middleware de errores, sin try/catch en cada handler.

¿Por qué estandarizar los errores?#

El formato application/problem+json de la RFC 9457 define campos comunes: type, title, status, detail e instance. Con ellos, clientes y SDKs tratan cualquier error de tu API de la misma manera, y nunca devuelves un stack trace: el error completo va al log y el cliente recibe solo el resumen.

Para el 429, envía la cabecera Retry-After con los segundos de espera. Los clientes bien escritos la respetan en lugar de insistir.

¿Cómo apagar la API sin cortar las peticiones en curso?#

En un deploy o reinicio, el proceso suele recibir SIGTERM. Sin manejo, muere en medio de las peticiones. En el código de arriba, server.close() deja de aceptar conexiones nuevas y cierra las inactivas, y las activas terminan. Pasados 10 segundos, closeAllConnections() fuerza el cierre. Los dos métodos existen en todas las versiones de Node.js que todavía tienen soporte.

Si tu API usa base de datos o cola, cierra esas conexiones dentro del callback de close().

Timeouts. Define un límite para la petición completa (server.requestTimeout) y un timeout propio en cada llamada que tu API hace a otros servicios. Sin eso, un servicio lento mantiene conexiones abiertas hasta que la API se queda sin recursos. En llamadas salientes, fetch(url, { signal: AbortSignal.timeout(5000) }) cancela a los 5 segundos.

¿Cómo verificar que todo funciona?#

Ejecuta API_TOKEN=secreto node server.mjs y prueba cada punto:

bash
curl -i localhost:3000/health
curl -i localhost:3000/items -X POST -H "Content-Type: application/json" -d '{"name":"a"}'
curl -i localhost:3000/items -X POST -H "Authorization: Bearer secreto" -H "Content-Type: application/json" -d '{"name":""}'
curl -i localhost:3000/items -X POST -H "Authorization: Bearer secreto" -H "Content-Type: application/json" -d '{"name":"lapiz"}'

Esperado: 200, 401, 400 y 201. Para el rate limit, repite una llamada más de 100 veces en un minuto y comprueba el 429 con Retry-After. Para el apagado, ejecuta kill -TERM <pid> con una petición lenta en curso y comprueba que termina antes de que el proceso salga.

¿Cuáles son los errores más comunes?#

  • Un health check que siempre responde 200: si la API depende de una base de datos, prueba la conexión en /health para que refleje la realidad.
  • CORS con * y credenciales: el navegador lo bloquea, y abrir todo "para que funcione" expone la API.
  • Rate limit por IP detrás de un proxy sin trust proxy: todos los clientes parecen la misma IP y comparten un solo límite.
  • Registrar el cuerpo de la petición: contraseñas y tokens terminan en texto plano en los logs.
  • Una copia de seguridad de la base de datos que nadie probó: prueba la restauración, no solo la creación de la copia.
  • Ningún monitor externo: si nadie mira /health, te enteras de la caída por los usuarios.

Cómo hacerlo en Square Cloud#

En Square Cloud haces el deploy de la API con un ZIP, la CLI, GitHub o la extensión de VS Code, y la plataforma ya resuelve parte del checklist. El resto sigue en tu código. Punto por punto:

  1. HTTPS: cada aplicación web recibe un subdominio *.squareweb.app con TLS automático, en todos los planes. El dominio propio empieza en Standard.
  2. Configuración: API_TOKEN y ALLOWED_ORIGIN se cargan como variables de entorno en el panel, la API o la CLI y valen desde el siguiente reinicio, sin rebuild. La plataforma define PORT, y server.mjs escucha en 0.0.0.0 en ese puerto.
  3. Disponibilidad: con el reinicio automático activado (lo predeterminado en el deploy desde el panel), la API vuelve sola cuando el proceso termina con error. Quedarse sin memoria, en cambio, detiene la aplicación hasta que aumentes su RAM y la inicies otra vez.
  4. Logs y métricas: las líneas JSON del log se ven en tiempo real en el panel, la CLI y VS Code, y las métricas de CPU, RAM y red se guardan 24 horas. Pro y Enterprise suman latencia p50/p95/p99 y logs HTTP.
  5. Protección: todos los planes incluyen WAF y protección DDoS L3/L4/L7, pero el rate limit por cliente sigue siendo tarea de la API.
  6. Varias instancias: el load balancing reparte el dominio entre aplicaciones sin sticky session y sin health check activo. Lleva los contadores del rate limit a Redis y mantén un monitor externo en /health. El load balancing y el hosting de Redis empiezan en Standard, desde R$44,99 al mes.
  7. Base de datos: las bases hospedadas reciben un snapshot automático diario, y un snapshot se restaura en una base nueva, donde pruebas la restauración sin tocar la de producción.

No hay autoscaling: para más capacidad, aumentas la RAM o sumas aplicaciones al load balancing. Consulta los planes en hosting de APIs y usa los SDKs oficiales para automatizar deploys, reinicios y variables de entorno.

Hosting de APIs

APIs en contenedores siempre encendidos, sin cold start

Más información

Preguntas frecuentes

¿Qué no puede faltar en una API en producción?

Configuración por variables de entorno, HTTPS, autenticación, validación de entrada, límite de peticiones, manejo de errores sin filtrar detalles internos, logs y un endpoint de health check.

¿Necesito un health check en una API pequeña?

Sí. Un endpoint simple que responde 200 permite que un monitor externo detecte cuándo la API se cayó, sin esperar a que un usuario se queje.

¿Qué status devuelvo cuando el cliente supera el límite de peticiones?

429 Too Many Requests, con la cabecera Retry-After que indica en cuántos segundos el cliente puede volver a intentarlo.

¿Puedo devolver al cliente el mensaje de error del servidor?

No. Los stack traces y los mensajes internos revelan estructura y dependencias. Registra el error completo en el log y devuelve al cliente solo un título y un status.

Siguiente paso · Paso 5 de 5Cómo leer los logs de tu aplicación (y encontrar el error)Para encontrar el error en los logs, empieza por el final, sube hasta la primera línea de error y lee el stack trace. Paso a paso en Node.js y Python.