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
- Seguridad: la validación del frontend se salta con una simple petición desde curl. Todo lo que llega a la API es dato no confiable.
- Integridad: evita que se guarden en la base de datos registros incompletos o con tipos incorrectos.
- Buenos mensajes: un 400 que dice exactamente qué campo falla le ahorra horas a quien consume tu API.
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 4 | Express 5 | |
|---|---|---|
| Error lanzado en una ruta síncrona | Llega al manejador | Llega 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