Estructura tu API en capas de responsabilidad única y conéctalas con una inyección ligera para hacerla testeable.
Abrir esta lección en KodokonUn único archivo que mezcla enrutamiento, lógica de negocio y acceso a datos siempre acaba saliendo caro: no puedes probar las reglas de negocio sin levantar un servidor, y no puedes cambiar de infraestructura sin reescribirlo todo. Dividir en tres capas resuelve ambos problemas. Las rutas declaran las URL y delegan. Los controladores traducen el HTTP a llamadas de negocio: leen la petición, eligen el código de estado, serializan la respuesta. Los servicios contienen las reglas de negocio y no saben absolutamente nada de Express. La regla de dependencia es estricta: cada capa conoce solo a la de abajo, nunca al revés.
import { Router } from 'express';
export function createUserRouter(controller) {
const router = Router();
router.get('/', controller.list);
router.post('/', controller.create);
return router;
}El controlador es una fábrica que recibe el servicio como argumento. No contiene ninguna regla de negocio: su trabajo se limita a extraer datos de req, llamar al servicio y convertir el resultado en una respuesta HTTP. Cualquier error se envía a next para que lo gestione el middleware de errores central, nunca un res.status(500) disperso por los controladores.
export function createUserController(service) {
return {
async list(req, res, next) {
try {
const users = await service.listUsers();
res.json(users);
} catch (err) {
next(err);
}
},
async create(req, res, next) {
try {
const user = await service.createUser(req.body);
res.status(201).json(user);
} catch (err) {
next(err);
}
},
};
}El servicio concentra las decisiones: unicidad del email, reglas de creación. Lanza errores de negocio (como mucho enriquecidos con un campo status) sin saber nada de Express. Él también recibe su dependencia, el repositorio de datos, como argumento. Esto es la inyección ligera: simples funciones fábrica llamadas una sola vez al arrancar reemplazan a los pesados contenedores de inyección, y hacen que cada capa sea intercambiable en las pruebas.
export function createUserService(repository) {
return {
listUsers() {
return repository.findAll();
},
createUser(input) {
const found = repository.findByEmail(input.email);
if (found) {
const err = new Error('Email already used');
err.status = 409;
throw err;
}
return repository.insert(input);
},
};
}Queda la raíz de composición: el único lugar donde encajan las fábricas. createApp recibe las dependencias de infraestructura (aquí la base de datos, tratada en la siguiente lección) y devuelve una aplicación Express completa que todavía no escucha. Separar la construcción de la escucha parece trivial; es exactamente lo que permitirá que las pruebas monten la API en un puerto efímero mientras el servidor de producción se queda en tres líneas: crear la base de datos, crear la app, escuchar.
import express from 'express';
import { createUserRepository } from './db/users.js';
import { createUserService } from './services/users.js';
import {
createUserController,
} from './controllers/users.js';
import { createUserRouter } from './routes/users.js';
export function createApp({ db }) {
const app = express();
app.use(express.json());
const repository = createUserRepository(db);
const service = createUserService(repository);
const controller = createUserController(service);
app.use('/api/users', createUserRouter(controller));
return app;
}createApp no se pone a escuchar en la red por sí mismo?