Un starter minimal pour une API REST en classes (Node.js + Express
- Prisma/PostgreSQL) entièrement câblée par injection de dépendances avec tsyringe. La gestion des utilisateurs sert d'exemple : l'objet du dépôt est l'architecture DI.
Ne fabrique pas tes dépendances - demande-les.
Aucune classe ne fait new de ses collaborateurs. Chaque classe déclare ses
dépendances dans son constructeur, et un conteneur assemble le graphe. Un
unique composition root (src/container.ts) déclare les
bindings ; tout le reste ne dépend que d'abstractions.
- Injection par constructeur sur trois couches :
UserController→UserService→IUserRepository. Chaque couche ignore comment ses dépendances sont fabriquées. - On dépend d'une interface, pas d'une classe.
UserServicedépend du contratIUserRepositoryvia un injection token (Symbol), parce qu'une interface n'existe pas au runtime. Deux implémentations (PrismaUserRepository,InMemoryUserRepository) se branchent sur le même token : changer de stockage tient en une ligne. PrismaClienten instance partagée (singleton) : un seul pool de connexions pour toute l'app. Le repository, sans état, est lui aussi en singleton.- Scope par requête via
ContainerScoped: un conteneur enfant par requête HTTP (src/server.ts) donne à chaque requête unRequestContextisolé - l'emplacement naturel pour l'utilisateur authentifié, un identifiant de trace ou une transaction. Singleton fuiterait entre requêtes, transient ne partagerait rien :ContainerScopedest le seul correct. - Services testés en isolation, sans base : on injecte un faux repository via un conteneur enfant (user.service.test.ts). C'est le bénéfice n°1 de la DI, et la raison d'être de tout ce découplage.
Node 22 · TypeScript · Express 5 · Prisma 6 / PostgreSQL · tsyringe 4 · Vitest.
Prérequis : Node 22+, un PostgreSQL accessible.
npm install
cp .env.example .env # puis ajuste DATABASE_URL
npx prisma migrate dev # crée la table User
npm run dev # API sur http://localhost:3000Frapper l'API :
curl -s localhost:3000/users
curl -s -X POST localhost:3000/users \
-H 'Content-Type: application/json' \
-d '{"email":"grace@hopper.dev","name":"Grace"}'Lancer les tests (aucune base requise) :
npx vitest runLe pattern se duplique mécaniquement. Pour une ressource Article :
- Modèle : ajoute
model Articledans prisma/schema.prisma, puisnpx prisma migrate dev. - Contrat + token + implémentation :
article.repository.tsavec une interfaceIArticleRepository, un tokenARTICLE_REPOSITORY = Symbol("ArticleRepository"), etPrismaArticleRepository implements IArticleRepositoryqui injectePrismaClient. - Service :
ArticleService(@injectable) qui injecte@inject(ARTICLE_REPOSITORY). - Contrôleur :
ArticleController(@injectable) qui injecteArticleService. - Binding :
container.registerSingleton(ARTICLE_REPOSITORY, PrismaArticleRepository)dans le composition root. - Routes : résous le contrôleur depuis
req.scope, comme pour/users.
Aucune de ces étapes ne touche au code existant : c'est le point de la DI.
Ce starter est minimal. Pour un vrai service, on ajouterait :
- Validation des entrées : un middleware
validateRequest(schema)(ex. zod). - Middleware d'erreurs centralisé : mapper les erreurs vers des codes HTTP.
- Validation des variables d'env au démarrage (fail-fast).
- Logs structurés.
- Arrêt gracieux : fermer le serveur et
PrismaClientsurSIGTERM. - Authentification :
RequestContextporterait l'utilisateur courant.
src/
container.ts composition root (bindings du conteneur)
server.ts Express + middleware de scope par requête
http/request-context.ts contexte ContainerScoped (1 par requête)
modules/user/
user.controller.ts couche HTTP (classe injectée)
user.service.ts logique métier (dépend du contrat)
user.repository.ts contrat IUserRepository + token + impl Prisma
in-memory-user.repository.ts seconde implémentation du même contrat
user.service.test.ts test en isolation (faux repo via conteneur enfant)
Vitest transpile par défaut avec esbuild, qui n'émet pas les métadonnées de
décorateur dont tsyringe a besoin. Le starter branche SWC
(vitest.config.ts) pour les générer - sans quoi @inject
échouerait en test.