El problema de la arquitectura prematura (y de la ausente)
Hay dos errores comunes al empezar un proyecto Node.js: sobre-arquitecturar desde el día uno con microservicios y capas de abstracción innecesarias, o poner todo en un archivo index.js de 800 líneas. Ninguno escala bien.
La estructura que voy a describir es la que uso en la mayoría de mis proyectos: lo suficientemente organizada para crecer, lo suficientemente simple para moverse rápido.
Estructura de carpetas
backend/
├── config/
│ └── db.js
├── middleware/
│ ├── auth.js
│ ├── errorHandler.js
│ └── validate.js
├── models/
│ └── User.js
├── routes/
│ └── users.js
├── controllers/
│ └── userController.js
├── services/
│ └── emailService.js
└── index.js
La diferencia entre routes, controllers y services
- Routes: solo definen el endpoint y apuntan al controller. Sin lógica.
- Controllers: reciben el request, llaman al servicio o al modelo, devuelven la response. Lógica HTTP solamente.
- Services: lógica de negocio pura. No saben nada de HTTP. Fáciles de testear.
// routes/users.js
import { Router } from 'express';
import { getUsers, createUser } from '../controllers/userController.js';
import { auth } from '../middleware/auth.js';
const router = Router();
router.get('/', auth, getUsers);
router.post('/', createUser);
export default router;
// controllers/userController.js
import { findAllUsers, createNewUser } from '../services/userService.js';
export const getUsers = async (req, res, next) => {
try {
const users = await findAllUsers(req.query);
res.json(users);
} catch (err) { next(err); }
};
Manejo global de errores
// middleware/errorHandler.js
export const errorHandler = (err, req, res, next) => {
const status = err.status || 500;
const message = err.message || 'Error interno del servidor';
if (process.env.NODE_ENV !== 'production') {
console.error(err.stack);
}
res.status(status).json({
error: message,
...(process.env.NODE_ENV !== 'production' && { stack: err.stack }),
});
};
// En index.js, siempre al final:
app.use(errorHandler);
Validación de requests
import Joi from 'joi';
export const validate = (schema) => (req, res, next) => {
const { error } = schema.validate(req.body, { abortEarly: false });
if (error) {
return res.status(400).json({
error: 'Validación fallida',
details: error.details.map(d => d.message),
});
}
next();
};
// Uso:
const createUserSchema = Joi.object({
name: Joi.string().min(2).required(),
email: Joi.string().email().required(),
password: Joi.string().min(8).required(),
});
router.post('/', validate(createUserSchema), createUser);
Paginación estándar
export const findAllUsers = async ({ page = 1, limit = 20, search }) => {
const query = search ? { name: new RegExp(search, 'i') } : {};
const skip = (page - 1) * limit;
const [items, total] = await Promise.all([
User.find(query).skip(skip).limit(Number(limit)).lean(),
User.countDocuments(query),
]);
return {
items,
pagination: { page: Number(page), limit: Number(limit), total, pages: Math.ceil(total / limit) },
};
};