Diseñar una API REST limpia con Node.js

Construir una API REST con Node.js puede parecer sencillo al principio, pero a medida que el proyecto crece la organización del código se vuelve crítica. En este artículo comparto una serie de pautas que me han ayudado a mantener mis APIs limpias, fáciles de probar y de evolucionar. No se trata de una receta única, sino de un conjunto de decisiones que, aplicadas de forma coherente, reducen la complejidad y mejoran la mantenibilidad.
1. Define claramente tus recursos
Antes de escribir una línea de código, identifica los recursos que expondrá tu API. Cada recurso debe corresponder a una entidad del dominio (por ejemplo, users, orders, products) y seguir la convención de nombres en plural. Esta claridad facilita la creación de rutas intuitivas y ayuda a los consumidores a entender rápidamente la estructura.
- Utiliza nombres en inglés y en plural.
- Evita verbos en la ruta; la acción se indica con el método HTTP.
- Ejemplo:
/api/v1/userspara listar o crear usuarios.
2. Separa la lógica de negocio del routing
Una trampa común es mezclar la lógica de negocio directamente dentro de los controladores de ruta. En su lugar, crea una capa de servicios que contenga toda la lógica relacionada con el dominio. De esta forma, los controladores solo se encargan de recibir la petición, delegar al servicio y devolver la respuesta.
const express = require('express');
const router = express.Router();
const userService = require('../services/userService');
router.get('/', async (req, res, next) => {
try {
const users = await userService.getAll();
res.json(users);
} catch (err) {
next(err);
}
});
module.exports = router;
En este fragmento, userService.getAll() contiene la lógica para acceder a la base de datos, mientras que el router se limita a orquestar la petición.
3. Controladores y rutas: mantén la coherencia
Organiza tus archivos de forma predecible. Una estructura típica podría ser:
src/
├─ routes/
│ ├─ userRoutes.js
│ └─ orderRoutes.js
├─ controllers/
│ ├─ userController.js
│ └─ orderController.js
├─ services/
│ ├─ userService.js
│ └─ orderService.js
└─ models/
├─ userModel.js
└─ orderModel.js
Con esta separación, cada capa tiene una responsabilidad única y el proyecto se vuelve más navegable.
4. Manejo de errores centralizado
En vez de capturar errores en cada controlador, delega la gestión a un middleware de errores. Así puedes unificar el formato de respuesta y registrar los fallos de manera consistente.
function errorHandler(err, req, res, next) {
console.error(err);
const status = err.status || 500;
res.status(status).json({
error: {
message: err.message || 'Error interno del servidor',
code: err.code || 'UNKNOWN'
}
});
}
app.use(errorHandler);
Los controladores solo deben pasar el error con next(err) y el middleware se encargará de la respuesta.
5. Validación de datos de entrada
Validar los datos antes de procesarlos evita errores inesperados y vulnerabilidades. Bibliotecas como Joi o zod permiten definir esquemas claros. Un ejemplo rápido con Joi:
const Joi = require('joi');
const createUserSchema = Joi.object({
name: Joi.string().min(3).required(),
email: Joi.string().email().required(),
password: Joi.string().min(8).required()
});
router.post('/', async (req, res, next) => {
try {
const value = await createUserSchema.validateAsync(req.body);
const user = await userService.create(value);
res.status(201).json(user);
} catch (err) {
next(err);
}
});
Al validar al inicio, mantienes la lógica de negocio limpia y segura.
6. Documentación automática con OpenAPI
Una API bien documentada facilita su adopción. Herramientas como swagger-jsdoc y swagger-ui-express generan una UI interactiva a partir de anotaciones en el código. Define un archivo swagger.json con los endpoints, parámetros y respuestas, y sirve la documentación en /api-docs. Así cualquier consumidor puede probar la API directamente desde el navegador.
En resumen, una API REST limpia con Node.js se basa en una arquitectura modular, separación de responsabilidades y manejo coherente de errores y validaciones. Adoptar estas prácticas desde el inicio ahorra tiempo de refactorización y hace que el proyecto sea mucho más agradable de mantener, tanto para ti como para el resto del equipo.