libro / blog / validacion-errores-express

// validación

Validación de datos y manejo de errores en Express

Gustavo Arias9 min de lectura

TL;DR Valida todo lo que entra con un esquema (por ejemplo, con Zod) y rechaza con 400 lo que no cumple. Lanza errores con su código HTTP desde cualquier parte y deja que un único middleware de errores los convierta en respuestas JSON. En Express 5 los errores de funciones async llegan solos a ese middleware.

Una API que funciona con los datos correctos es la mitad del trabajo. La otra mitad es qué pasa cuando los datos no son correctos, la base de datos no responde o algo inesperado falla. Si no lo decidís vos, lo decide Express, y probablemente con un HTML de error que tu frontend no sabe interpretar. En el libro Node.js desde cero la validación y el manejo de errores tienen un capítulo propio; acá tenés un enfoque completo y ordenado.

Por qué validar en el servidor

Validar con un esquema

Escribir if a mano para cada campo se vuelve inmanejable. Una librería de esquemas como Zod te deja describir la forma de los datos una vez y validar con una línea:

import { z } from 'zod';

export const crearPostSchema = z.object({
  titulo: z.string().trim().min(3, 'Mínimo 3 caracteres').max(120),
  contenido: z.string().min(10, 'El contenido es demasiado corto'),
  etiquetas: z.array(z.string().max(30)).max(5).default([]),
  publicado: z.boolean().default(false),
});

Y un middleware reutilizable que valida cualquier cuerpo contra cualquier esquema:

export const validar = (schema) => (req, res, next) => {
  const resultado = schema.safeParse(req.body ?? {});

  if (!resultado.success) {
    return res.status(400).json({
      error: 'Datos inválidos',
      detalles: resultado.error.issues.map((i) => ({ campo: i.path.join('.'), mensaje: i.message })),
    });
  }

  req.body = resultado.data;   // datos limpios: con trim, valores por defecto y sin campos extra
  next();
};

// Uso: router.post('/', validar(crearPostSchema), crearPost);

Un detalle valioso: por defecto, Zod descarta los campos que no están en el esquema. Si alguien envía "rol": "admin" en el cuerpo, ese campo no llega a tu código.

Errores con código HTTP

Para que cualquier parte del código pueda decir “esto es un 404” o “esto es un 409”, definí una clase de error con el código incluido:

export class HttpError extends Error {
  constructor(status, mensaje, detalles) {
    super(mensaje);
    this.status = status;
    this.detalles = detalles;
  }
}

export const noEncontrado = (que = 'Recurso') => new HttpError(404, `${que} no encontrado`);
import { noEncontrado } from '../errors.js';
import { posts } from '../db.js';

export async function verPost(req, res) {
  const post = await posts.buscarPorId(req.params.id);
  if (!post) throw noEncontrado('Post');   // Express 5 lo envía al manejador de errores
  res.json(post);
}

Un manejador de errores central

Un middleware con cuatro parámetros es, para Express, un manejador de errores. Va al final de todo, después de las rutas:

import { HttpError } from '../errors.js';

// eslint-disable-next-line no-unused-vars -- Express necesita los 4 parámetros
export function manejadorDeErrores(err, req, res, next) {
  // JSON mal formado en el cuerpo (lo lanza express.json())
  if (err.type === 'entity.parse.failed') {
    return res.status(400).json({ error: 'El cuerpo no es un JSON válido' });
  }

  // Errores previstos: se muestran tal cual
  if (err instanceof HttpError) {
    return res.status(err.status).json({ error: err.message, detalles: err.detalles });
  }

  // Errores imprevistos: detalle al log, mensaje genérico al cliente
  console.error(`[${req.id ?? '-'}]`, err);
  res.status(500).json({ error: 'Error interno del servidor' });
}
app.use('/api/posts', postsRouter);
app.use((req, res) => res.status(404).json({ error: 'Ruta no encontrada' }));
app.use(manejadorDeErrores);   // siempre el último

Resultado: todas las respuestas de error de tu API tienen el mismo formato, y ningún stack trace llega al cliente.

Errores asíncronos: Express 4 vs Express 5

Express 4Express 5
Error lanzado en una ruta síncronaLlega al manejadorLlega al manejador
Error en una ruta async (promesa rechazada)No llega: hay que capturarlo y llamar a next(err)Llega automáticamente

Si mantenés un proyecto con Express 4, envolvé tus rutas asíncronas:

export const asyncHandler = (fn) => (req, res, next) =>
  Promise.resolve(fn(req, res, next)).catch(next);

// router.get('/:id', asyncHandler(verPost));

Errores que tumban el proceso

Un error que nadie captura fuera de Express (en un temporizador, en un evento) puede terminar el proceso. Registralo y dejá que el gestor de procesos lo reinicie:

process.on('unhandledRejection', (motivo) => {
  console.error('Promesa rechazada sin capturar:', motivo);
  process.exit(1);   // el estado puede ser inconsistente: mejor reiniciar limpio
});

process.on('uncaughtException', (err) => {
  console.error('Excepción no capturada:', err);
  process.exit(1);
});

Quien vuelve a levantar la aplicación es PM2, Docker o tu plataforma de hosting, como vemos en despliegue en producción.

APIs robustas desde el diseño

El libro Node.js desde cero dedica un capítulo a la validación de datos y el manejo de errores, y otro a la seguridad en aplicaciones Node.js.

Comprar en Amazon