Kodokon kodokon.com

Architecturer une API : routes, contrôleurs, services

Structurez votre API en couches à responsabilité unique et assemblez-les par injection légère pour la rendre testable.

10 min · 3 questions

Ouvrir cette leçon dans Kodokon

Un fichier unique qui mélange routage, logique métier et accès aux données finit toujours par coûter cher : impossible de tester le métier sans lancer un serveur, impossible de changer d'infrastructure sans tout réécrire. Le découpage en trois couches répond à ces deux problèmes. Les routes déclarent les URL et délèguent. Les contrôleurs traduisent HTTP en appels métier : lire la requête, choisir le code de statut, sérialiser la réponse. Les services portent les règles métier et ignorent totalement Express. La règle de dépendance est stricte : chaque couche ne connaît que celle du dessous, jamais l'inverse.

JAVASCRIPT
import { Router } from 'express';

export function createUserRouter(controller) {
  const router = Router();
  router.get('/', controller.list);
  router.post('/', controller.create);
  return router;
}
La route déclare, le contrôleur agit : aucune logique ici.

Le contrôleur est une fabrique qui reçoit le service en argument. Il ne contient aucune règle métier : son travail se limite à extraire les données de req, appeler le service et transformer le résultat en réponse HTTP. Toute erreur part vers next pour être traitée par le middleware d'erreur central - jamais de res.status(500) dispersé dans les contrôleurs.

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);
      }
    },
  };
}
Traduction HTTP pure : statuts, sérialisation, délégation.

Le service concentre les décisions : unicité de l'email, règles de création. Il lève des erreurs métier - au plus enrichies d'un champ status - sans rien savoir d'Express. Lui aussi reçoit sa dépendance, le dépôt de données, en argument. C'est l'injection légère : de simples fonctions fabriques appelées une fois au démarrage remplacent les conteneurs d'injection lourds, et rendent chaque couche substituable dans les tests.

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);
    },
  };
}
Le métier vit ici, sans req, res ni SQL.

Reste la racine de composition : l'unique endroit où les fabriques s'emboîtent. createApp reçoit les dépendances d'infrastructure (ici la base de données, détaillée à la prochaine leçon) et retourne une application Express complète mais pas encore à l'écoute. Séparer la construction de l'écoute paraît anodin ; c'est ce qui permettra aux tests de monter l'API sur un port éphémère, pendant que le serveur de production reste trois lignes : créer la base, créer l'app, écouter.

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 seule zone de l'application qui connaît tout le monde.

Quiz de validation

Vérifiez que vous avez bien retenu les points clés de cette leçon.

  1. Dans ce découpage, où doit vivre la règle « un email ne peut être utilisé qu'une seule fois » ?
    • Dans la route, au plus près de l'URL
    • Dans le contrôleur, qui a accès à req.body
    • Dans le service, qui porte les règles métier
    • Dans un middleware Express global
  2. Quel est l'intérêt principal de passer les dépendances en arguments de fabriques plutôt que de les importer directement dans chaque module ?
    • Les fabriques accélèrent le chargement des modules
    • On peut substituer une dépendance (base en mémoire, faux dépôt) sans toucher au module qui l'utilise
    • Express exige ce style pour ses middlewares
    • Cela évite d'écrire des fichiers séparés
  3. Pourquoi createApp ne démarre-t-il pas lui-même l'écoute réseau ?
    • Parce qu'Express interdit d'appeler listen dans une fonction
    • Pour que les tests instancient l'application sans ouvrir de port fixe, l'entrée serveur restant triviale
    • Parce que listen est asynchrone et bloquerait la fabrique
    • Pour réduire la consommation mémoire au démarrage