Structurez votre API en couches à responsabilité unique et assemblez-les par injection légère pour la rendre testable.
Ouvrir cette leçon dans KodokonUn 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.
import { Router } from 'express';
export function createUserRouter(controller) {
const router = Router();
router.get('/', controller.list);
router.post('/', controller.create);
return router;
}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.
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);
}
},
};
}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.
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);
},
};
}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.
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 ne démarre-t-il pas lui-même l'écoute réseau ?