J1 · Fondations Node et Nest
Avant d'écrire du Nest, il faut changer de modèle d'exécution : un seul thread JavaScript sert toutes les requêtes, et tout ce qui attend (base de données, réseau, fichiers) passe par des promesses. Ce chapitre pose ces bases, génère le projet TaskFlow, démarre PostgreSQL avec Docker Compose et expose GET /health. À la fin, vous saurez expliquer pourquoi une boucle synchrone de 5 s bloque toute l'API, et comment l'éviter.
Objectifs
- Comprendre l'event loop de Node : phases, microtasks, macrotasks, et ce qui bloque le thread.
- Écrire du code asynchrone avec
Promiseetasync/awaitsans perdre d'erreurs. - Distinguer modules ES et CommonJS, et savoir lequel votre projet utilise.
- Générer un projet avec la Nest CLI et lire sa structure :
main.ts,AppModule,AppController,AppService. - Démarrer PostgreSQL et Adminer avec Docker Compose, exposer
GET /health, initialiser le dépôt git.
Spring → NestJS
| Spring | NestJS / Node | À retenir |
|---|---|---|
SpringApplication.run(App.class, args) | NestFactory.create(AppModule) puis app.listen(port) | Deux étapes : construire le conteneur, puis ouvrir le port |
Classe @SpringBootApplication | AppModule (@Module) | Racine du graphe de modules, sans component scan |
| Pool de threads Tomcat (un thread par requête) | Un thread JavaScript et l'event loop | Un calcul synchrone bloque toutes les requêtes |
CompletableFuture, Mono | Promise, async/await | await suspend la fonction, pas le thread |
Thread.sleep(5000) | await sleep(5000) (node:timers/promises) | L'attente libère le thread pour les autres requêtes |
ExecutorService.invokeAll() | Promise.all() | La concurrence se pense en promesses, pas en threads |
@Async | Rien d'équivalent : async ne change pas de thread | Une fonction async tourne sur le même thread jusqu'à son premier await |
| Spring Initializr | nest new | Génère le projet, installe les dépendances, initialise git |
Maven / Gradle, pom.xml | npm, package.json + package-lock.json | npm ci installe exactement ce que fixe le lockfile |
mvn spring-boot:run + devtools | npm run start:dev (nest start --watch) | Recompile et redémarre à chaque modification |
/actuator/health | GET /health (Terminus en J11) | Hors versioning : pas de préfixe /v1 |
Comprendre
Event loop, microtasks et macrotasks
Node exécute votre code JavaScript sur un seul thread. Les entrées-sorties (sockets, requêtes SQL, fichiers) sont confiées au système d'exploitation ou à libuv, la bibliothèque C de Node. Quand une opération se termine, son callback est mis en file. L'event loop est la boucle qui prend ces callbacks un par un et les exécute sur ce thread unique.
Chaque tour de boucle traverse des phases, chacune avec sa propre file :
| Phase | Ce qui s'y exécute |
|---|---|
| timers | callbacks de setTimeout et setInterval arrivés à échéance |
| pending callbacks | certains callbacks d'erreurs système reportés au tour suivant |
| idle, prepare | usage interne de Node |
| poll | callbacks d'I/O (socket lisible, réponse PostgreSQL reçue) ; la boucle y attend quand il n'y a rien d'autre à faire |
| check | callbacks de setImmediate |
| close callbacks | fermetures, par exemple socket.on('close') |
Ces callbacks sont des macrotasks. Après chacun d'eux, Node vide entièrement les files de microtasks : d'abord process.nextTick, puis les continuations de promesses (.then, la suite d'un await). Une microtask passe donc toujours avant le prochain timer ou la prochaine I/O.
// Exemple hors TaskFlow : un fichier CommonJS lancé avec node
setTimeout(() => console.log('4. macrotask (timers)'), 0);
Promise.resolve().then(() => console.log('3. microtask (promise)'));
process.nextTick(() => console.log('2. process.nextTick'));
console.log('1. synchrone');La sortie suit les numéros. Dans un module ES, 3 s'affiche avant 2 : l'évaluation d'un module ES a lieu elle-même dans une microtask, et la file des promesses est vidée avant celle de nextTick. Ne bâtissez pas de logique sur cet ordre fin. Retenez la règle : code synchrone, puis microtasks, puis macrotasks.
La conséquence directe : tant que du code synchrone tourne, rien d'autre ne tourne. Ni les autres requêtes HTTP, ni les timers.
// Exemple hors TaskFlow
const start = Date.now();
setTimeout(() => console.log(`timer après ${Date.now() - start} ms`), 10);
const end = Date.now() + 200;
while (Date.now() < end) {} // 200 ms de calcul synchroneLe timer demandé à 10 ms s'exécute après environ 200 ms : setTimeout donne un délai minimal, pas une échéance garantie.
Côté Spring MVC, chaque requête a son thread Tomcat : un appel bloquant n'immobilise que lui. Ici, une boucle CPU, une API *Sync (fs.readFileSync, crypto.pbkdf2Sync) ou un JSON.parse sur un corps énorme dans un handler bloque toutes les requêtes en cours. Le modèle Java le plus proche est WebFlux sur Netty : peu de threads d'event loop, qu'il ne faut jamais bloquer. Pour un vrai calcul lourd, déportez-le dans des worker_threads ou dans une queue (J9).
Pas tout à fait un seul thread
libuv garde un petit pool de threads interne (4 par défaut) pour les opérations sans API asynchrone native côté système : accès fichiers, dns.lookup, une partie de crypto, zlib. Votre code JavaScript, lui, tourne toujours sur un seul thread.
Pour la vue d'ensemble des différences de modèle d'exécution, voir Différences majeures. L'exercice du jour les rend visibles.
Promise et async/await
Une Promise représente un résultat futur : en attente, puis résolue (une valeur) ou rejetée (une erreur). Une fonction async renvoie toujours une Promise. await suspend la fonction jusqu'au règlement de la promesse et rend la main à l'event loop, qui sert d'autres requêtes en attendant ; la suite de la fonction reprend plus tard, sous forme de microtask. Une promesse rejetée devient une exception à l'endroit du await : try/catch fonctionne comme en Java.
Deux différences avec Java méritent d'être retenues tôt :
asyncne crée pas de thread. Une fonctionasyncs'exécute de façon synchrone jusqu'à son premierawait. La marquerasyncne la déporte nulle part, contrairement à@Async. Une boucle CPU dans une fonctionasyncbloque autant qu'ailleurs.- La concurrence s'écrit avec
Promise.all, pas avec unExecutorService. Les requêtes partent ensemble et le thread reste libre pendant l'attente.
// Exemple hors TaskFlow
// Séquentiel : la seconde requête ne part qu'après la première.
const user = await usersRepo.findOneBy({ id });
const tasks = await tasksRepo.findBy({ assigneeId: id });
// Concurrent : les deux requêtes partent ensemble, durée totale = la plus lente.
const [owner, assigned] = await Promise.all([
usersRepo.findOneBy({ id }),
tasksRepo.findBy({ assigneeId: id }),
]);Promise.all échoue dès la première promesse rejetée ; Promise.allSettled attend tout et renvoie le statut de chacune.
Le bug classique est l'await oublié :
// Exemple hors TaskFlow
async create(dto: CreateProjectDto) {
this.repository.save(dto); // await oublié
return { ok: true }; // la réponse part avant l'INSERT
}La réponse part avant l'écriture, et si l'INSERT échoue, l'erreur n'arrive à personne : ni au try/catch de l'appelant, ni au filtre d'exceptions de Nest. Depuis Node 15, une promesse rejetée que rien n'attend arrête même le processus par défaut. Le projet généré par la CLI 12 le détecte au lint : il utilise oxlint, dont la configuration active typescript/no-floating-promises (la même règle que @typescript-eslint/no-floating-promises cité par le plan). Un appel save(); sans await y est signalé en erreur. TaskFlow garde cette configuration :
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"rules": {
"typescript/no-explicit-any": "off",
"typescript/no-floating-promises": "error"
},
"env": {
"node": true
}
}Quand une promesse ne doit volontairement pas être attendue, écrivez-le avec void : c'est ce que fait void bootstrap(); dans main.ts. Voir aussi Pièges fréquents.
Modules ES et CommonJS
Node connaît deux systèmes de modules :
| CommonJS (CJS) | Modules ES (ESM) | |
|---|---|---|
| Syntaxe exécutée | require(), module.exports | import, export |
| Activé par | défaut (.js sans "type": "module", ou .cjs) | "type": "module" dans package.json, ou .mjs |
| Chargement | synchrone | asynchrone, await permis au niveau du module |
| Imports relatifs | extension facultative (./app.module) | extension obligatoire (./app.module.js) |
__dirname, __filename | disponibles | absents (import.meta.dirname à la place) |
En TypeScript, vous écrivez toujours import / export. C'est le compilateur qui produit l'un ou l'autre format. Avec "module": "nodenext" (le réglage de TaskFlow), il décide fichier par fichier d'après le package.json le plus proche : sans "type": "module", il émet des require().
TaskFlow est en CommonJS : son package.json n'a pas de champ type, et dist/main.js utilise require. Cela se voit dans le code : postgres-options.ts et data-source.ts utilisent __dirname, et le CLI TypeORM est lancé par typeorm-ts-node-commonjs (J5).
Les schematics de la CLI 12 installés avec TaskFlow (@nestjs/schematics 12.0.7) demandent le système de modules à la création du projet : ESM avec Vitest (choix par défaut) ou CommonJS avec Jest. Le plan et TaskFlow utilisent Jest (J10) : choisissez CommonJS. La variante ESM se reconnaît aux imports relatifs suffixés en .js et à await bootstrap(); au niveau du module.
Les erreurs typiques d'un mélange des deux :
SyntaxError: Cannot use import statement outside a module: du code ESM chargé comme CommonJS.ReferenceError: require is not defined in ES module scope(ou__dirname is not defined) : du code CommonJS chargé comme ESM.- Un paquet publié uniquement en ESM, importé depuis du CommonJS : les versions récentes de Node, dont Node 24, acceptent
require()d'un module ES sansawaitau niveau du module ; les plus anciennes échouaient avecERR_REQUIRE_ESM.
Il n'y a pas d'équivalent Java direct : le classpath ne connaît qu'un format. Pensez à vérifier le champ type du package.json avant de copier un exemple trouvé en ligne.
Structure d'un projet Nest et démarrage
nest new en variante CommonJS génère ceci :
taskflow/
├── src/
│ ├── app.controller.spec.ts test unitaire du contrôleur
│ ├── app.controller.ts
│ ├── app.module.ts
│ ├── app.service.ts
│ └── main.ts point d'entrée
├── test/
│ ├── app.e2e-spec.ts test e2e (supertest)
│ └── jest-e2e.json
├── .gitattributes
├── .gitignore écrit par la CLI avec git init
├── .oxlintrc.json
├── .prettierrc
├── README.md
├── jest.config.ts
├── nest-cli.json
├── package.json
├── tsconfig.json
└── tsconfig.build.jsonLes trois classes générées montrent le découpage de Nest. Elles ont disparu de TaskFlow, remplacées par un module par domaine :
// Généré par nest new (absent de TaskFlow)
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}
@Module({
imports: [],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}AppControllerest un@RestController:@Get()sans chemin répond àGET /.AppServiceest un@Service:@Injectable()le rend gérable par le conteneur.constructor(private readonly appService: AppService)déclare et assigne le champ en une ligne (une « parameter property » TypeScript, l'équivalent d'un@RequiredArgsConstructorLombok). Nest retrouve le typeAppServicegrâce aux métadonnées émises par le compilateur, détaillées en J3.AppModuledéclare explicitement ses contrôleurs et providers. Il n'y a pas de component scan : une classe absente de tout module n'existe pas pour Nest (J3).
Le point d'entrée remplace SpringApplication.run. Celui de TaskFlow :
// SpringApplication.run(TaskflowApplication.class, args)
async function bootstrap() {
const app = await NestFactory.create(AppModule, { bufferLogs: true });
setupApp(app);
setupSwagger(app);
await app.listen(app.get(ConfigService).getOrThrow<number>('PORT'));
}
// `void` marks the promise as deliberately not awaited (no-floating-promises).
void bootstrap();NestFactory.create(AppModule)part du module racine, parcourt sesimports, résout les dépendances et instancie providers et contrôleurs, au-dessus de l'adaptateur HTTP (Express 5). CommeSpringApplication.run, il construit le conteneur ; contrairement à lui, il n'ouvre pas de port.setupApp(app)etsetupSwagger(app)appliquent les réglages HTTP qui ne sont pas des providers : logger,helmet, CORS, versioning (J2), documentation (J11).bufferLogs: trueretient les logs de démarrage jusqu'à ce que le logger pino soit branché (J7).app.listen(port)initialise l'application si ce n'est pas fait (enregistrement des routes, hooksonModuleInitetonApplicationBootstrap, voir J7), puis ouvre le port. Le port vient de la configuration validée ; la version générée litprocess.env.PORT ?? 3000.void bootstrap();lance la promesse sans l'attendre, explicitement. En CommonJS, pas d'awaitau niveau du module : d'où la fonction.
Pour un script sans serveur HTTP (un batch, un worker), NestFactory.createApplicationContext(AppModule) construit le conteneur sans adaptateur HTTP, comme une application Spring Boot non web.
Au-delà du squelette, TaskFlow range le code par domaine (projects/, tasks/, users/…) avec des dossiers transverses (common/, config/, database/) : voir l'arborescence et les conventions de nommage.
Nest CLI
La CLI génère le projet et les briques, en les déclarant dans le bon module :
npm i -g @nestjs/cli
nest new taskflow # projet complet : fichiers, npm install, git init
nest g module projects # alias : nest g mo projects
nest g controller projects # alias : nest g co projects
nest g service projects # alias : nest g s projects
nest g resource projects # module, contrôleur, service, DTO et entité d'un coup
nest g service projects --dry-run # affiche les fichiers sans rien écrirenest g moduleajoute le nouveau module auximportsdu module parent (AppModulepar défaut) ;nest g controlleretnest g serviceajoutent la classe auxcontrollers/providersdu module le plus proche.--skip-importdésactive cette déclaration automatique.--flatcrée les fichiers sans sous-dossier,--no-specsans fichier de test.nest g resourcepose des questions (REST ou GraphQL, points d'entrée CRUD) : utilisé en J2.nest start --watch(scriptstart:dev) recompile et redémarre à chaque sauvegarde ;nest buildcompile versdist/en appliquant les plugins déclarés dansnest-cli.json.
Les fichiers générés sont un point de départ : vous les modifiez librement ensuite, la CLI ne les régénère pas.
Toutes les commandes au même endroit
L'aide-mémoire Nest CLI liste les schematics et leurs alias (gu pour guard, itc pour interceptor…), indique lesquels sont déclarés automatiquement dans un module, et donne les commandes qui produisent le squelette de TaskFlow. Tout y est vérifié sur le CLI 12.
Cas TaskFlow
Générer le projet
npm i -g @nestjs/cli
nest new taskflow --package-manager npm --no-observe
cd taskflow
npm run start:dev # puis http://localhost:3000 → "Hello World!"- À la question du système de modules, choisissez CJS (CommonJS) avec Jest pour suivre le plan (modules ES et CommonJS).
--no-observeévite la proposition d'installer@nestjs/observe, que TaskFlow n'utilise pas.- La CLI installe les dépendances, lance
git initet écrit un.gitignore(sauf avec--skip-git).
Ce que TaskFlow a gardé du projet généré : tsconfig.json (module: nodenext, strict, experimentalDecorators et emitDecoratorMetadata, indispensables à l'injection), la configuration oxlint et les scripts Jest, qui tournent avec node --experimental-vm-modules. AppController et AppService ont été supprimés au profit des modules de domaine.
Docker Compose : PostgreSQL et Adminer
Le fichier de TaskFlow reprend celui du plan, avec des profils pour les services optionnels :
# docker compose up -d -> PostgreSQL + Adminer for `npm run start:dev`
# docker compose --profile queue up -d -> + Redis for the BullMQ queue (J9)
# docker compose --profile app up -d -> + the API itself, built from the Dockerfile (J11)
services:
db:
image: postgres:16-alpine
container_name: taskflow-db
environment:
POSTGRES_USER: taskflow
POSTGRES_PASSWORD: taskflow
POSTGRES_DB: taskflow
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U taskflow -d taskflow"]
interval: 5s
timeout: 3s
retries: 10
# Throwaway database for e2e tests: data lives in memory (tmpfs).
db-test:
image: postgres:16-alpine
container_name: taskflow-db-test
environment:
POSTGRES_USER: taskflow
POSTGRES_PASSWORD: taskflow
POSTGRES_DB: taskflow_test
ports:
- "5433:5432"
tmpfs:
- /var/lib/postgresql/data
adminer:
image: adminer
ports:
- "8081:8080"
depends_on:
- db
redis:
image: redis:7-alpine
profiles: [queue]
ports:
- "6379:6379"
app:
build: .
profiles: [app]
environment:
NODE_ENV: production
PORT: 3000
DB_HOST: db
DB_PORT: 5432
DB_USER: taskflow
DB_PASSWORD: taskflow
DB_NAME: taskflow
# Empty unless exported: the app then refuses to start (validateEnv), while
# the other services still work without it.
JWT_SECRET: ${JWT_SECRET:-}
ports:
- "3000:3000"
depends_on:
db:
condition: service_healthy
volumes:
pgdata:| Service | Rôle | Accès depuis votre machine |
|---|---|---|
db | PostgreSQL 16 de développement, données dans le volume pgdata | localhost:5432 |
db-test | Base jetable pour les tests e2e, données en mémoire (tmpfs) | localhost:5433 |
adminer | Interface web pour PostgreSQL | http://localhost:8081 |
redis | File BullMQ, profil queue (J9) | localhost:6379 |
app | L'API conteneurisée, profil app (J11) | localhost:3000 |
docker compose up -d # db, db-test et adminer (les profils restent arrêtés)
docker compose ps # attendre que db soit "healthy"
docker compose logs -f db
docker compose down # arrête les conteneurs, garde le volume pgdataDans Adminer, choisissez le système PostgreSQL et le serveur db, pas localhost : Adminer tourne dans un conteneur et joint la base par le réseau de Compose. Utilisateur, mot de passe et base : taskflow. Votre application Nest, lancée sur la machine avec npm run start:dev, joint la même base sur localhost:5432.
Les identifiants doivent correspondre à ceux du fichier d'environnement. Copiez le modèle versionné :
cp .env.example .env.development# Copy to .env.development (npm run start:dev) and .env.test (npm run test:e2e).
# Only this example file is versioned.
NODE_ENV=development
PORT=3000
DB_HOST=localhost
DB_PORT=5432
DB_USER=taskflow
DB_PASSWORD=taskflow
DB_NAME=taskflow
JWT_SECRET=change-me
JWT_EXPIRES_IN=15m
# typeorm (PostgreSQL) or memory (the J2-J3 in-memory store)
PROJECTS_STORE=typeorm
# Set to enable the BullMQ notifications queue (docker compose --profile queue up -d)
# REDIS_URL=redis://localhost:6379
# Comma-separated origins allowed by CORS; unset = no cross-origin access
# CORS_ORIGIN=http://localhost:5173Le chargement et la validation de ces variables sont le sujet de J7 ; les profils Compose et les variables sont résumés dans Docker Compose et .env. Spring Boot 3.1 et plus sait démarrer un fichier Compose au lancement de l'application (module spring-boot-docker-compose) ; ici, vous le démarrez vous-même.
GET /health
Le livrable du jour est un endpoint qui répond { status: 'ok' }, utile aux load balancers, aux orchestrateurs et au HEALTHCHECK Docker. Version minimale :
// Exemple hors TaskFlow : la version J1, remplacée par Terminus en J11
import { Controller, Get, Module } from '@nestjs/common';
@Controller('health')
export class HealthController {
@Get()
check() {
return { status: 'ok' };
}
}
@Module({ controllers: [HealthController] })
export class HealthModule {}Ajoutez HealthModule aux imports d'AppModule (ou générez-le avec nest g module health puis nest g controller health), puis :
curl http://localhost:3000/health{ "status": "ok" }La version finale de TaskFlow (J11)
En J11, le contrôleur passe à @nestjs/terminus : /health/live (liveness, sans dépendance) et /health ou /health/ready (readiness : base, mémoire, disque). La réponse garde status: "ok" et ajoute le détail de chaque indicateur.
// /actuator/health/liveness and /readiness. VERSION_NEUTRAL keeps them at
// /health/... instead of /v1/health/... for load balancers and Kubernetes.
@ApiTags('health')
@Public()
@SkipThrottle()
@Controller({ path: 'health', version: VERSION_NEUTRAL })
export class HealthController {
constructor(
private readonly health: HealthCheckService,
private readonly db: TypeOrmHealthIndicator,
private readonly memory: MemoryHealthIndicator,
private readonly disk: DiskHealthIndicator,
) {}
/** Liveness: the process answers. No dependency, or a DB outage restarts every pod. */
@Get('live')
@HealthCheck()
live() {
return this.health.check([]);
}
/** Readiness: ready to receive traffic. */
@Get(['', 'ready'])
@HealthCheck()
ready() {
return this.health.check([
() => this.db.pingCheck('database', { timeout: 1_500 }),
() => this.memory.checkHeap('memory_heap', 512 * 1024 * 1024),
() => this.disk.checkStorage('disk', { path: '/', thresholdPercent: 0.9 }),
]);
}
}VERSION_NEUTRAL garde l'URL /health une fois le versioning /v1 activé, @Public() la dispense de JWT et @SkipThrottle() du rate limiting. Explications complètes : J11.
Dépôt git, README et LEARNINGS
nest new a déjà lancé git init et écrit un .gitignore. Celui-ci ignore .env et les fichiers .env.*.local, mais pas .env.development ni .env.test, que le plan ne versionne pas. Ajoutez .env* puis !.env.example (la négation doit venir après). TaskFlow a son propre .gitignore, dans examples/, qui ne dépend pas de celui du dépôt du site :
node_modules/
dist/
coverage/
*.log
*.tsbuildinfo
.env*
!.env.exampleComplétez ensuite le dépôt avec :
README.md: comment démarrer (Compose,.env.development,npm run start:dev) et les scripts. Celui de TaskFlow sert de modèle.LEARNINGS.md: le journal de fin de journée (ce qui diffère de Spring, les pièges, les questions ouvertes). TaskFlow en contient le gabarit ; la méthode est décrite dans le plan.
git add .
git commit -m "feat: squelette TaskFlow, Docker Compose et /health"
git tag jour-01Exercice : /block contre /ping
L'exercice du plan est conservé dans TaskFlow, sous /demo :
// The J1 exercise. Call /demo/block, then /demo/ping in another terminal:
// ping waits ~5 s, because the only JavaScript thread is busy looping.
// With /demo/sleep, ping answers at once: awaiting frees the thread.
@Public()
@SkipThrottle()
@Controller({ path: 'demo', version: VERSION_NEUTRAL })
export class EventLoopDemoController {
@Get('ping')
ping() {
return { pong: new Date().toISOString() };
}
@Get('block')
block() {
const end = Date.now() + 5_000;
while (Date.now() < end) {
// Synchronous CPU work: nothing else can run meanwhile.
}
return { blockedMs: 5_000 };
}
@Get('sleep')
async sleep() {
await sleep(5_000); // The thread serves other requests during the wait.
return { sleptMs: 5_000 };
}
}Démarrez l'application (docker compose up -d, .env.development en place, npm run start:dev), puis utilisez deux terminaux :
# Terminal 1 : occupe le thread JavaScript pendant 5 s
curl http://localhost:3000/demo/block
# Terminal 2, aussitôt après : mesure le temps de réponse de /ping
curl -w '\n%{time_total} s\n' http://localhost:3000/demo/pingRecommencez en remplaçant /demo/block par /demo/sleep. Mesures relevées sur TaskFlow :
| Pendant | Temps de réponse de /demo/ping | Pourquoi |
|---|---|---|
/demo/block | 4,7 s | La boucle while occupe le seul thread ; /ping attend la fin de la boucle (le temps restant après son lancement) |
/demo/sleep | ~1 ms | await sleep(5_000) rend la main à l'event loop, qui sert /ping immédiatement |
Avec Spring MVC, /ping aurait répondu tout de suite dans les deux cas, servi par un autre thread du pool. Ici, la seule protection est de ne jamais bloquer le thread : await pour les I/O, worker_threads ou une queue pour le calcul.
Quelques détails du contrôleur, expliqués plus tard : @Public() dispense du JWT (J8), @SkipThrottle() du rate limiting (J11), et VERSION_NEUTRAL garde les URL hors du préfixe /v1. AppModule ne charge ce module qu'en dehors de la production, via ConditionalModule.registerWhen(DemoModule, (env) => env.NODE_ENV !== 'production').
Situations
- Une requête lente ralentit toutes les autres → cherchez du code synchrone dans le handler : boucle, API
*Sync, grosJSON.parse(event loop, exercice). - Vous devez attendre deux appels indépendants →
await Promise.all([...])plutôt que deuxawaitsuccessifs (async/await). - Une erreur disparaît, ou une écriture arrive après la réponse → un
awaitmanque ; lancez le lint (no-floating-promises) (async/await). Cannot use import statement outside a moduleourequire is not defined→ vérifiez le champtypedupackage.jsonet la cible du compilateur (modules).- Vous démarrez un nouveau service Nest →
nest newen CommonJS si vous testez avec Jest (générer le projet). - Vous ajoutez un module, un contrôleur ou un service →
nest g mo|co|s <nom>, qui le déclare dans le bon module (CLI). - Il vous faut un PostgreSQL local et un outil pour le consulter →
docker compose up -d, Adminer sur le port 8081 avec le serveurdb(Compose). - Un load balancer ou Docker doit savoir si l'API répond → un endpoint
/healthhors versioning (health, J11).
Pièges
Bloquer l'event loop
Tout calcul synchrone dans un handler bloque toutes les requêtes : boucle CPU, fs.readFileSync, crypto.pbkdf2Sync, sérialisation d'un très gros objet. Utilisez les versions asynchrones des API, et déportez le calcul lourd dans des worker_threads ou une queue (J9).
Promesse non attendue
this.repository.save(dto); sans await : la réponse part avant l'écriture, l'erreur éventuelle n'est vue par personne, et une promesse rejetée non gérée arrête le processus (Node 15+). Gardez la règle no-floating-promises en erreur et marquez les exceptions voulues avec void.
docker compose down -v efface la base
L'option -v supprime aussi les volumes, donc pgdata et toutes les données de développement. docker compose down seul les conserve.
array.forEach(async (item) => { await save(item); })n'attend rien :forEachignore les promesses renvoyées. Utilisezfor (const item of array) { await save(item); }pour du séquentiel, ouawait Promise.all(array.map((item) => save(item)))pour du concurrent.setTimeout(fn, 0)ne veut pas dire « maintenant » : le callback attend la fin du code synchrone et des microtasks, et toute boucle bloquante le retarde d'autant.- Le
.gitignoregénéré ignore.envmais pas.env.developmentni.env.test: ajoutez.env*et!.env.exampleavant le premier commit qui contient un secret. - Dans Adminer, le serveur
localhostdésigne le conteneur Adminer lui-même : utilisezdb. nest newpropose ESM avec Vitest par défaut. Si vous suivez le plan (Jest), choisissez CommonJS ; sinon, les imports relatifs devront porter l'extension.jset les exemples Jest ne s'appliqueront pas tels quels.
Exercices
- [ ]
npm i -g @nestjs/cli, puisnest new taskflow(CommonJS, npm). - [ ] Lire
main.ts,app.module.ts,app.controller.ts,app.service.tset noter l'équivalent Spring de chaque fichier (SpringApplication.run↔NestFactory.create). - [ ] Créer
docker-compose.yml(PostgreSQL + Adminer),docker compose up -d, se connecter dans Adminer. - [ ] Exposer
GET /healthrenvoyant{ status: 'ok' }et le vérifier aveccurl. - [ ] Compléter le dépôt git :
.gitignore(avec.env*et!.env.example),README.md,LEARNINGS.md, premier commit,git tag jour-01. - [ ] Exercice de compréhension : un endpoint
/blockqui boucle 5 s de façon synchrone et un endpoint/ping. Observer que/pingest bloqué, puis remplacer la boucle parawait sleep(5000)et comparer les temps. - [ ] Exécuter le script
setTimeout/Promise/nextTickde la section event loop, en.cjspuis en.mjs, et expliquer la différence d'ordre. - [ ] Écrire une fonction qui lance deux appels avec
Promise.all, puis la même avec deuxawaitsuccessifs, et comparer les durées. - [ ] Noter dans
LEARNINGS.mdce qui change par rapport au modèle un thread par requête de Spring MVC.