Kodokon kodokon.com

Arquitectura de una API: rutas, controladores, servicios

Estructura tu API en capas de responsabilidad única y conéctalas con una inyección ligera para hacerla testeable.

10 min · 3 preguntas

Abrir esta lección en Kodokon

Un ú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.

JAVASCRIPT
import { Router } from 'express';

export function createUserRouter(controller) {
  const router = Router();
  router.get('/', controller.list);
  router.post('/', controller.create);
  return router;
}
La ruta declara, el controlador actúa: aquí no hay lógica.

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.

JAVASCRIPT
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);
      }
    },
  };
}
Pura traducción HTTP: códigos de estado, serialización, delegación.

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.

JAVASCRIPT
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);
    },
  };
}
La lógica de negocio vive aquí, sin req, res ni SQL.

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.

JAVASCRIPT
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;
}
app.js: la única parte de la aplicación que conoce a todos.

Prueba de conocimientos

Comprueba que has retenido los puntos clave de esta lección.

  1. En este esquema de capas, ¿dónde debería vivir la regla "un email solo se puede usar una vez"?
    • En la ruta, lo más cerca posible de la URL
    • En el controlador, que tiene acceso a req.body
    • En el servicio, que contiene las reglas de negocio
    • En un middleware global de Express
  2. ¿Cuál es la principal ventaja de pasar las dependencias como argumentos de fábrica en lugar de importarlas directamente en cada módulo?
    • Las fábricas aceleran la carga de los módulos
    • Puedes cambiar una dependencia (base de datos en memoria, repositorio falso) sin tocar el módulo que la usa
    • Express exige este estilo para sus middlewares
    • Evita tener que escribir archivos separados
  3. ¿Por qué createApp no se pone a escuchar en la red por sí mismo?
    • Porque Express prohíbe llamar a listen dentro de una función
    • Para que las pruebas puedan instanciar la aplicación sin abrir un puerto fijo, manteniendo trivial el punto de entrada del servidor
    • Porque listen es asíncrono y bloquearía la fábrica
    • Para reducir el uso de memoria al arrancar