API protegida (auth + validación + rate limiting)
Antes de empezar
Sección titulada «Antes de empezar»Esta receta ensambla piezas de una aplicación Express: autenticación, roles, Prisma y manejo de errores. Los imports locales proceden de esas guías; adapta nombres de campos al modelo real. No es una aplicación autónoma lista para publicar.
El orden de las capas importa
Sección titulada «El orden de las capas importa»import { Router } from "express"import rateLimit from "express-rate-limit"import { z } from "zod"import { prisma } from "../lib/prisma"import { requireAuth } from "../middlewares/requireAuth"import { requireRole } from "../middlewares/requireRole"import { asyncHandler } from "../utils/asyncHandler"
export const adminRouter = Router()
const limiterEscritura = rateLimit({ windowMs: 15 * 60 * 1000, max: 30 })
const actualizarUsuarioSchema = z.strictObject({ rol: z.enum(["user", "moderador", "admin"]).optional(), activo: z.boolean().optional()}).refine(data => Object.keys(data).length > 0, "Envía al menos un campo")
adminRouter.patch( "/usuarios/:id", requireAuth, // 1. ¿quién eres? (401 si no hay token válido) requireRole("admin"), // 2. ¿puedes hacer esto? (403 si no eres admin) limiterEscritura, // 3. ¿estás abusando de este endpoint? (429 si sí) asyncHandler(async (req, res) => { const datos = actualizarUsuarioSchema.parse(req.body) // 4. ¿el body tiene forma válida? (400 si no)
const usuario = await prisma.user.update({ where: { id: req.params.id }, data: datos, select: { id: true, rol: true, activo: true } })
res.json(usuario) }))Por qué ese orden específico
Sección titulada «Por qué ese orden específico»límite general → reduce tráfico antes de autenticación costosarequireAuth → verifica identidad; puede consultar base de datos o criptografíarequireRole → aplica permisos a esa identidadlimiterEscritura → limita esta operación; aquí usa la IP por defectovalidación → comprueba parámetros y campos antes de escribirEl costo depende de la implementación: autenticarse no siempre es barato. Aplica un límite general antes de autenticación y uno específico para operaciones sensibles. express.json() ya habrá parseado el cuerpo si se montó globalmente; el límite de tamaño debe configurarse allí. Para limitar por usuario, configura una clave basada en la identidad verificada y un almacén compartido si hay varias instancias.
CORS a nivel de la app, no de esta ruta puntual
Sección titulada «CORS a nivel de la app, no de esta ruta puntual»import cors from "cors"import { adminRouter } from "./routes/admin.routes"
app.use( cors({ origin: process.env.FRONTEND_URL, credentials: true }))app.use(helmet())app.use(express.json({ limit: "1mb" }))
app.use("/admin", adminRouter)app.use(errorHandler)CORS y helmet van a nivel de toda la app (una vez), no repetidos por ruta — son protecciones transversales, a diferencia de auth/rol/rate-limit que sí varían según qué tan sensible es cada endpoint puntual.
Manejar errores de validación
Sección titulada «Manejar errores de validación»export const errorHandler: ErrorRequestHandler = (err, req, res, next) => { if (err instanceof ZodError) { return res.status(400).json({ error: { code: "VALIDATION_ERROR", fields: err.flatten().fieldErrors } }) } if (err instanceof AppError) { return res .status(err.status) .json({ error: { code: err.code, message: err.message } }) } console.error(err) res .status(500) .json({ error: { code: "INTERNAL_ERROR", message: "Error interno" } })}(express-rate-limit responde 429 por sí solo antes de que la request llegue al error handler — no necesita manejo especial ahí.)
Checklist de una ruta bien protegida
Sección titulada «Checklist de una ruta bien protegida»| Capa | Protege contra |
|---|---|
requireAuth | Acceso sin autenticar |
requireRole / ownership | Acceso autenticado pero sin permiso suficiente |
| Rate limiting | Abuso por volumen, fuerza bruta |
| Validación (Zod) | Datos con forma inesperada o maliciosa |
| CORS + Helmet (nivel app) | Exposición del lado del navegador, headers faltantes |
Consideraciones
Sección titulada «Consideraciones»- Valida también el identificador de la ruta según el modelo (UUID, CUID u otro) y traduce el error de registro inexistente de Prisma a 404. Usa
selectpara no devolver hashes de contraseña u otros campos privados. AppError,errorHandler,helmet,expressyapppertenecen al ensamblado de las guías previas; los bloques de esta receta muestran los cambios relevantes, no repiten esos archivos completos.- Si la sesión viaja en cookies, añade protección CSRF a las operaciones de escritura. CORS no autentica ni reemplaza la comprobación de origen/token CSRF.
Comprobación
Sección titulada «Comprobación»Comprueba 401 sin sesión, 403 con rol insuficiente, 400 con cuerpo vacío o campos extra, 404 con id inexistente y 429 al superar el límite. En la respuesta correcta solo deben aparecer id, rol y activo. Una operación rechazada no debe modificar la base de datos.
- No todas las rutas necesitan las cinco capas — un endpoint público de solo lectura (
GET /posts) no necesitarequireAuth; el nivel de protección depende de qué tan sensible es cada operación puntual. - Esta receta combina piezas ya documentadas por separado — para el detalle de cada una (por qué ese orden de middlewares, cómo funciona cada pieza), ver sus guías individuales enlazadas en
technologiesarriba.