Skip to content

Migration Nest 7 → 8 → 9 → 10 → 11

TL;DR — Migrer Nest entre majors n'est jamais "drop-in" mais c'est rarement catastrophique. Le vrai risque vient des deps tierces (TypeORM, Mongoose, Passport, Swagger, throttler, cache-manager) qui bumpent en parallèle avec des breaking changes plus durs que Nest lui-même. Règle : un major à la fois, lance les tests, fix, commit. Et ne saute pas Node — chaque major Nest impose une version Node mini.


Quick reference table

NestReleasedNode minTS minExpressFastifyRxJSNotes clés
72020-0210.133.74.x2.x6.xPre-modern. ESM N/A.
82021-07124.34.x3.x7.xBreaking: RxJS 7, removal of HttpModule legacy.
92022-08124.74.x4.x7.xNew Logger, durable providers, faster CLI.
102023-07164.84.x4.x7.xDrop Node 12/14. cache-manager v5. axios via @nestjs/axios.
112025-01205.15.x5.x7.xExpress 5, Fastify 5, ESM-friendly, Logger objects accepted.

Modèle mental — ce que tu migres vraiment

Le piège du débutant est de croire qu'on migre Nest. Faux. @nestjs/core et @nestjs/common sont remarquablement stables entre majors : 80% des breaking changes que tu vivras viennent de l'écosystème que Nest ré-exporte ou wrappe. Mentalement, sépare trois couches :

CoucheExemplesStabilité entre majorsQui dicte le breaking
RuntimeNode, engines, libuv, V8Imposé (Node min monte)TC39 / Node LTS schedule
Core Nest@nestjs/core, @nestjs/common, DI, lifecycleTrès stableÉquipe Nest (rare, documenté)
Adapters & intégrationsTypeORM, Mongoose, Passport, Apollo, cache-manager, throttler, multer, Express/FastifyInstable — c'est ici que ça casseMainteneurs tiers, désynchronisés de Nest

Conséquence staff : le numéro de version Nest est un proxy trompeur du risque. Migrer Nest 9→10 paraît anodin, mais si ce major embarque cache-manager v4→v5 (TTL en secondes → ms, API sync → async), le risque réel est dans la dep tierce, pas dans Nest. Avant chaque major, dresse la matrice de compatibilité des deps (cf. tableaux ci-dessous) et classe chaque ligne en mécanique (sed/codemod), sémantique (le comportement change à code identique — le plus dangereux) ou architectural (refactor de pattern). Les bugs sémantiques silencieux (TTL qui passe de 60s à 60ms, erreurs middleware qui remontent au lieu d'être avalées) sont ceux qui passent les tests verts et explosent en prod.

Comment un staff engineer raisonne sur "un major à la fois"

Le mantra "un major à la fois" n'est pas de la prudence rituelle, c'est de la réduction de l'espace de debug. Si tu bumpes Nest + TypeORM + Node simultanément et qu'un test devient rouge, ton espace de causes possibles est le produit cartésien des trois changements. En isolant chaque major dans sa propre PR mergée et déployée, tu transformes une recherche O(N×M×K) en O(N)+O(M)+O(K). C'est git bisect appliqué à l'échelle des dépendances : chaque PR est un commit bisectable en staging.

Corollaire : la vraie unité de migration n'est pas "le major Nest" mais "le breaking change sémantique le plus risqué de ce major". Pour 8→9, ce n'est pas Nest, c'est TypeORM 0.2→0.3 — donc tu le détaches en PR autonome même si ça veut dire deux PRs pour un major.


Nest 7 → 8

What's new

  • RxJS 7toPromise() déprécié, utiliser firstValueFrom() / lastValueFrom().
  • @nestjs/config@1registerAs, schema validation Joi support officiel.
  • CLI : webpack hot reload amélioré.
  • @nestjs/swagger@5 — renommage massif (voir breaking).
  • GraphQL code-first stabilisé.

Breaking changes

Avant (Nest 7)Après (Nest 8)
HttpModule from @nestjs/commonHttpModule from @nestjs/axios
obs.toPromise()firstValueFrom(obs)
@ApiModelProperty@ApiProperty
@ApiModelPropertyOptional@ApiPropertyOptional
Microservices PORT option stringnumber required

Upgrade steps

bash
pnpm up "@nestjs/*@8" "rxjs@7"
pnpm add @nestjs/axios   # was bundled before
# codemod RxJS
npx ng-update @rxjs/update@7
# replace @ApiModel* manually (sed or find/replace)

Tests à relancer en priorité : tout ce qui utilise HttpService et les controllers décorés Swagger.


Nest 8 → 9

What's new

  • Durable providers (Scope.REQUEST avec durable: true) — perfs énormément améliorées pour le multi-tenant.
  • New Logger API — accepte un contexte structuré, setLogLevels(['error', 'warn']).
  • CLI plugin : meilleur introspection des DTO pour Swagger.
  • @nestjs/microservices : Kafka client mis à jour (kafkajs 2.x).
  • TypeORM v3 (Nest n'oblige pas) → la migration TypeORM 0.2 → 0.3 est l'éléphant dans la pièce — breaking massif côté DataSource, connection options.

Breaking changes

Avant (Nest 8)Après (Nest 9)
getConnection() (TypeORM 0.2)DataSource injected (TypeORM 0.3)
Connection injection tokenDataSource
@nestjs/typeorm@8@nestjs/typeorm@9 (compatible TypeORM 0.3)
@Catch() sans arg = warning@Catch() capture tout (silencieux)

Upgrade steps

bash
pnpm up "@nestjs/*@9" "@nestjs/typeorm@9" "[email protected]"
# refactor: Connection -> DataSource everywhere
# refactor: getRepository(X) -> dataSource.getRepository(X)
# entities config: now array, not glob string by default

⚠️ La migration TypeORM 0.2 → 0.3 mérite sa propre PR séparée. Combine-la avec Nest 9 seulement si tu maîtrises les deux.


Nest 9 → 10

What's new

  • Drop Node 12 & 14 — Node 16 minimum (Node 18 recommandé).
  • @nestjs/cli@10 : Webpack 5 par défaut, builds plus rapides.
  • @nestjs/cache-manager : module séparé, cache-manager@5 (API entirely Promise-based).
  • @nestjs/throttler@5 : multi-throttlers (short/medium/long).
  • enableShutdownHooks non bloquant sur les transient providers.
  • GraphQL : Apollo Server v4 (breaking dans la config).

Breaking changes

Avant (Nest 9)Après (Nest 10)
cache.set(key, val, { ttl: 60 }) (sec)cache.set(key, val, 60_000) (ms)
CacheModule from @nestjs/commonCacheModule from @nestjs/cache-manager
Throttle({ default: { ... } })Throttle({ short: { ... } }) ou nom personnalisé
GraphQLModule.forRoot({ ... }) Apollo v3Apollo v4 (driver: ApolloDriver)
Node 12/14Node 16 mini, Node 18 recommandé

Upgrade steps

bash
# 1) bump node version (CI + .nvmrc)
nvm install 18 && nvm use 18

# 2) bump Nest
pnpm up "@nestjs/*@10"
pnpm add @nestjs/cache-manager cache-manager@5

# 3) cache TTL refactor (sec -> ms)
# find . -name "*.ts" | xargs grep "cache.set" -l

# 4) throttler config refactor
# 5) Apollo Server v4 migration if GraphQL

pnpm test

⚠️ Le passage cache-manager v4 → v5 est le plus piégeux. L'API devient async partout, et le TTL change d'unité. Si tu as 50 appels à cache.set, prévois une journée.


Nest 10 → 11

What's new

  • Node 20 minimum (Node 18 dropped).
  • Express 5 support officiel — async error propagation native, plus de boilerplate try/catch.
  • Fastify 5 support officiel.
  • Logger accepte objets : logger.log({ event, userId }).
  • TypeScript 5.1+ — meilleur inference, using (Symbol.dispose) supporté.
  • CLI : SWC compiler en option (@swc/core), 10–20x plus rapide que ts-loader.
  • RxJS reste 7 — pas de 8 yet.
  • Module reloading (HMR) amélioré en dev.

Breaking changes

Avant (Nest 10)Après (Nest 11)
Node 18 OKNode 20 minimum
Express 4 defaultExpress 5 default — middleware async errors propagated différently
@nestjs/config@3 get() retourne T@nestjs/config@4 get() retourne T | undefined, use getOrThrow
Fastify 4Fastify 5 — addContentTypeParser signature mineure change
TS 4.8+TS 5.1+
multer typings bundledmulter peer dep explicite

Upgrade steps

bash
nvm install 20 && nvm use 20
pnpm up "@nestjs/*@11" "typescript@5"
pnpm add @nestjs/config@4 fastify@5  # if using Fastify
# refactor: cfg.get<string>('X') -> cfg.getOrThrow<string>('X')
# review: any middleware that swallowed errors in Express 4 might now bubble up
pnpm test && pnpm e2e

⚠️ Express 5 change la propagation d'erreurs dans les middleware. Si tu as du code Express middleware tiers (passport-XXX), vérifier sa compat. Souvent il faut wrapper.


  1. Audit depspnpm outdated --long (ou npm outdated). Note les majors qui bumpent en parallèle.
  2. Pin Node au max supporté par ta version courante (ex. Nest 7 → Node 14 LTS). C'est la baseline saine avant de bumper Nest.
  3. Tests verts d'abord — si tu as 30% de coverage, migrer est suicidaire. Couvre les chemins critiques avant.
  4. Un major à la fois :
    • 7 → 8 : Swagger renaming + RxJS 7 (1–2 jours).
    • 8 → 9 : détacher TypeORM 0.2 → 0.3 en PR séparée si possible (3–5 jours).
    • 9 → 10 : cache-manager v5 + Node 18 (1–2 jours).
    • 10 → 11 : Node 20 + Express 5 + config@4 (1 jour).
  5. CI verts — chaque major mergé séparément, déployé en staging, observé 24–48h avant le prochain.
  6. Rollback plan — pin exact + tag git par version. pnpm-lock.yaml commité.

Tooling helpers

  • @nestjs/schematics + nest update : limite (officiellement déprécié pour les majors), mais utile sur les configs.
  • npm-check-updates : ncu -i interactif.
  • @nx/migrate si Nx : nx migrate latest génère un fichier migrations.json à exécuter.
  • Renovate / Dependabot : configuré en CI pour automatiser les minor/patch après migration.

Common cross-version pain points

TypeORM 0.2 → 0.3 (during Nest 8→9 or 9→10)

Le plus douloureux. Connection devient DataSource. getRepository() est déprécié. Repositories custom (@EntityRepository) supprimés → préfère le pattern repository injecté via dataSource.getRepository().

ts
// avant
@EntityRepository(User)
export class UsersRepository extends Repository<User> {}

// après
@Injectable()
export class UsersRepository {
  constructor(@InjectDataSource() private ds: DataSource) {}
  private get repo() { return this.ds.getRepository(User); }
}

Passport ecosystem

[email protected] (Nest 9+) change la signature de req.login() (async-only). Les guards Passport doivent gérer la promise.

GraphQL Apollo v3 → v4 (Nest 10)

ts
// avant
GraphQLModule.forRoot({ autoSchemaFile: 'schema.gql' })

// après
GraphQLModule.forRoot<ApolloDriverConfig>({
  driver: ApolloDriver,
  autoSchemaFile: 'schema.gql',
})

Express 4 → 5 (Nest 11)

  • req.params est null-prototype object (pas Object.prototype). Affecte sérialisation/lodash.
  • Async middleware errors propagés automatiquement → try/catch historiques deviennent redondants mais non-bloquants.
  • res.redirect('back') supprimé → utiliser req.get('Referrer').

RxJS 6 → 7 (Nest 7 → 8)

  • toPromise() déprécié.
  • Imports paths : rxjs/operators toujours OK, mais types stricts.

Production concerns — déployer une migration sans tout casser

Les tests verts ne prouvent qu'une chose : que tes hypothèses connues tiennent. Une migration majeure change des comportements que tu n'as pas pensé à tester. La stratégie de déploiement compte autant que le diff.

Observabilité différentielle — comparer avant/après, pas juste "ça marche"

Avant de merger un major, capture une baseline de métriques sur la version courante. Après déploiement, tu ne cherches pas "est-ce que ça plante" mais "qu'est-ce qui a dérivé". Les régressions de migration sont rarement des 500 ; ce sont des p99 qui doublent, un GC qui s'emballe, un endpoint qui passe de 12ms à 40ms parce que Express 5 a changé le parsing.

ts
// Instrumente les chemins critiques avec un histogramme labellisé par version applicative.
// Le label `app_version` (injecté au build) permet de comparer deux releases côte à côte en Grafana.
import { Histogram } from 'prom-client';

const httpLatency = new Histogram({
  name: 'http_request_duration_seconds',
  help: 'Latence des requêtes HTTP',
  labelNames: ['method', 'route', 'status', 'app_version'],
  buckets: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5],
});

// Dashboard de migration : tracer p50/p95/p99 groupés par app_version.
// PromQL : histogram_quantile(0.99,
//   sum(rate(http_request_duration_seconds_bucket[5m])) by (le, app_version))

Métriques à surveiller spécifiquement par major :

MajorMétrique qui dérive en silencePourquoi
9→10 (cache-manager v5)cache hit ratio s'effondreTTL en ms mal converti → expiration immédiate, tout passe en miss
10→11 (Express 5)latence p99 du parsing bodynouveau body-parser, req.params null-prototype
8→9 (TypeORM 0.3)nombre de connexions au poolDataSource change la gestion du pool par défaut
10→11 (Node 20)RSS / heap baselinenouveau V8, GC tuning différent

Canary / déploiement progressif — le vrai filet de sécurité

Le "staging 48h" du plan ci-dessus est nécessaire mais insuffisant : staging n'a pas le trafic de prod. Pour un service à fort volume, déploie la nouvelle version en canary (5% du trafic) derrière le même load balancer, avec rollback automatique si le taux d'erreur ou la latence p99 dévie du baseline.

yaml
# Argo Rollouts — canary d'une migration majeure avec analyse automatique
apiVersion: argoproj.io/v1alpha1
kind: Rollout
spec:
  strategy:
    canary:
      steps:
        - setWeight: 5
        - pause: { duration: 10m }
        - analysis:
            templates: [{ templateName: error-rate-and-latency }]
            # rollback auto si error-rate > 1% ou p99 > baseline * 1.2
        - setWeight: 25
        - pause: { duration: 30m }
        - setWeight: 50
        - pause: { duration: 1h }
        - setWeight: 100

Contract testing — la vraie garantie pour une API consommée

Si ton service Nest est consommé par d'autres équipes (frontend Angular, microservices, partenaires), tes tests unitaires ne protègent pas leurs clients. Une migration peut changer la sérialisation (Express 5 + req.params null-prototype affecte JSON.stringify de certains payloads ; class-transformer peut changer entre majors). Verrouille le contrat :

ts
// Snapshot du contrat OpenAPI — diff le schéma généré avant/après migration.
// Un changement non intentionnel de shape = breaking pour les consommateurs.
import { SwaggerModule } from '@nestjs/swagger';
import { writeFileSync, readFileSync } from 'node:fs';

const doc = SwaggerModule.createDocument(app, config);
const previous = JSON.parse(readFileSync('contracts/openapi.snapshot.json', 'utf8'));
// En CI : si diff(doc, previous) contient un removal/rename non whitelisté → fail.
writeFileSync('contracts/openapi.current.json', JSON.stringify(doc, null, 2));

Pour les consommateurs internes, un Pact (consumer-driven contract) entre l'Angular et le NestJS attrape la régression côté consommateur avant le déploiement. Règle staff : une migration backend qui change le contrat sans bump de version d'API est un incident, pas une migration.

Sécurité — la dette CVE est la vraie raison de migrer

Une version Nest EOL ne reçoit plus de patches de sécurité, ni Nest ni ses transitives. npm audit / pnpm audit doit faire partie de la CI de migration : souvent le déclencheur business d'une migration n'est pas "on veut Fastify", c'est "une CVE critique dans une transitive n'est patchée que sur le major suivant". Documente le diff de CVE résolues dans la PR — c'est l'argument qui débloque le budget direction (cf. Scénario 1).


Decision tree — "should I upgrade now?"

Are you on Nest 7?  ──── YES ──► Plan migration urgently. Security patches stopped.

       NO

Nest 8 or 9?  ──── YES ──► Migrate within 6 months. TypeORM 0.2 EOL is the killer.

       NO

Nest 10?  ──── YES ──► Stable. Migrate to 11 within 12 months for Node 20 perf.

       NO

Nest 11. Stay current with minor/patch. Watch for Nest 12 (2026?).

🎬 Cas d'usage concrets

Scénario 1 — Upgrade d'un SaaS RH legacy Nest 7 → Nest 11

Qui : éditeur d'un SaaS de gestion de paie, code stuck en Nest 7 + TypeORM 0.2 + Node 14 depuis 2020. Plus aucun patch de sécurité Node, vulns CVE non patchées dans lodash, passport. Problème : direction sécurité bloque les nouveaux clients du secteur public tant que la stack n'est pas alignée. 6 majors d'écart en 4 ans. La peur paralysait l'équipe.

bash
# Plan en 5 PRs séparées étalées sur 6 semaines
# Sprint 1 : couverture tests sur les 20 endpoints critiques (avant migration)
pnpm jest --coverage  # baseline 38% → target 65%

# Sprint 2 : Nest 7 → 8 (1 jour)
pnpm up "@nestjs/*@8" "rxjs@7"
pnpm add @nestjs/axios   # detached from common
# codemod manual sed
grep -rl "@ApiModelProperty" src | xargs sed -i 's/@ApiModelProperty/@ApiProperty/g'
grep -rl "@ApiModelPropertyOptional" src | xargs sed -i 's/@ApiModelPropertyOptional/@ApiPropertyOptional/g'
grep -rl "\.toPromise()" src | xargs sed -i 's/\.toPromise()/\.then(v => firstValueFrom(of(v)))/g'  # then fix manually

# Sprint 3 : Nest 8 → 9 + TypeORM 0.2 → 0.3 (PR séparée, 4 jours)
pnpm up "@nestjs/*@9" "[email protected]" "@nestjs/typeorm@9"
# Refactor: @InjectConnection() → @InjectDataSource()
# Refactor: @EntityRepository() supprimés → repositories injectés

# Sprint 4 : Nest 9 → 10 (Node 18 + cache-manager v5, 2 jours)
nvm install 18 && nvm alias hr-app 18
pnpm up "@nestjs/*@10" "cache-manager@5" "@nestjs/cache-manager"
# Refactor TTL: { ttl: 60 } sec → 60_000 ms

# Sprint 5 : Nest 10 → 11 (Node 20 + Express 5 + config@4, 1 jour)
nvm install 20
pnpm up "@nestjs/*@11" "[email protected]" "@nestjs/config@4"
grep -rl "this.cfg.get<string>" src | xargs sed -i 's/this.cfg.get<string>/this.cfg.getOrThrow<string>/g'

Gains : 6 semaines au total (estimation initiale : 6 mois). 0 incident en prod après chaque release (staging 48h, observation, puis prod). Vulns CVE résolues, sécurité débloquée pour le marché public. Bonus : Node 20 + Fastify a réduit la latence p99 de 220ms à 140ms sans toucher au code applicatif.

Scénario 2 — Migration FinTech Express → Fastify dans Nest 11

Qui : néobanque PME sur Nest 10 + Express 4. Pic à 8K req/s sur l'API publique, Express bottleneck identifié au profiling. Problème : passer à Fastify implique 2 majors : Nest 11 (qui supporte Express 5 par défaut, mais Fastify 5 aussi) + remplacement de tous les req: Request typés Express. Plusieurs middlewares Passport custom.

ts
// Avant — Nest 10 + Express 4
import { Request } from 'express';
@Get(':id')
findOne(@Param('id') id: string, @Req() req: Request) {
  const ip = req.ip;
  return this.svc.find(id);
}

// Étape 1 — Nest 11 + Express 5 (juste pour le bump Node 20)
// Code presque identique, juste vérifier le comportement async errors

// Étape 2 — switch Fastify
import { FastifyRequest } from 'fastify';
import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify';

// main.ts
const app = await NestFactory.create<NestFastifyApplication>(
  AppModule,
  new FastifyAdapter({ logger: false, trustProxy: true }),
);

// controllers — replace Request type
@Get(':id')
findOne(@Param('id') id: string, @Req() req: FastifyRequest) {
  const ip = req.ip;
  return this.svc.find(id);
}

// passport — ensure @fastify/passport instead of passport directly
// helmet/cors — replace express middleware with @fastify/* counterparts
import helmet from '@fastify/helmet';
import cors from '@fastify/cors';
app.register(helmet);
app.register(cors, { origin: corsOrigins });

Gains : Fastify bench montre +75% throughput sur les endpoints lecture. Le coût pods K8s a diminué de 35%. La migration a pris 2 sprints (4 semaines) : une pour Nest 11 + Express 5, une pour basculer Fastify avec validation continue.

Scénario 3 — Bump majeur d'une e-commerce productionnée

Qui : marketplace e-commerce, 25K SKU, 4 services Nest en monorepo. Stuck en Nest 9 depuis 2022, 80K LOC, équipe 6 devs. Problème : bug récurrent en prod sur les cache-manager v3 (memory leaks observés sur les workers BullMQ). Le bump à cache-manager v5 (donc Nest 10+) bloqué par la peur du Big Bang.

bash
# Stratégie: feature branches longue-durée + intégration progressive
git checkout -b chore/nest-10
pnpm up "@nestjs/*@10" "cache-manager@5" "@nestjs/cache-manager"

# Refactor systématique
grep -rl "CacheModule" src libs apps | xargs sed -i 's|from .@nestjs/common.|from "@nestjs/cache-manager"|g'

# TTL units
grep -rln "cache\.set(.*,.*,.*{.*ttl:" src libs apps  # find them
# fix manually: { ttl: 60 } -> 60_000

# CI green on the branch before merge
nx affected -t lint,test,build --base=main

# Apollo v3 → v4 (GraphQL was used in 2 apps)
pnpm up "@nestjs/graphql@11" "@apollo/server@4"
# refactor: GraphQLModule.forRoot({ driver: ApolloDriver, ... })

# Rolling deployment per app
# Day 1: deploy catalog-api in staging, observe 48h
# Day 3: deploy in prod
# Day 4: orders-api, etc.

Gains : le bug memory leak résolu (cache-manager v5 expose proprement les TTL et nettoie ses entries). Apollo v4 a apporté un boost perf GraphQL (+30% throughput sur la page produit). La migration s'est étalée sur 8 semaines mais sans downtime ni regression majeure.

🛠️ Exemple end-to-end

Mise en situation : tu pilotes la migration d'un projet Nest 8 + TypeORM 0.2 + Node 14 vers Nest 11 + TypeORM 0.3 + Node 20. Le projet a 5 apps (en Nx monorepo), 12 libs, 35K LOC, 42% de coverage initial. Tu veux : un plan en PRs séparées, un script de vérification qui valide chaque étape, un changelog interne pour l'équipe, et un plan de rollback documenté.

bash
#!/usr/bin/env bash
# scripts/migrate-nest.sh — orchestrate the migration step by step
set -euo pipefail

step() { echo "===== $1 ====="; }

step "0. Pre-flight checks"
node -v   # expect v14 or v16
git diff --quiet || { echo "Uncommitted changes"; exit 1; }
pnpm exec nx affected -t test --base=origin/main || { echo "Tests not green"; exit 1; }

step "1. Improve coverage on critical paths"
# Manual: add tests until coverage > 65% on payments, auth, orders
pnpm exec nx run-many -t test --coverage --projects=payments-api,auth-api,orders-api

step "2. Migrate to Nest 8 (if on 7)"
pnpm up "@nestjs/*@8" "rxjs@7" --recursive
pnpm add @nestjs/axios -r
echo "Run codemod for @ApiModelProperty → @ApiProperty (manual)"

step "3. Migrate to Nest 9 + TypeORM 0.3 (one PR)"
pnpm up "@nestjs/*@9" "@nestjs/typeorm@9" "[email protected]" -r
echo "Refactor Connection → DataSource, @EntityRepository → injected pattern"

step "4. Bump Node to 18, then 20"
echo "Update .nvmrc to 18.x first"
echo "Update Dockerfile FROM node:18-alpine → node:20-alpine"
echo "Update CI .github/workflows/*.yml node-version"

step "5. Migrate to Nest 10 (cache-manager v5 + Apollo v4)"
pnpm up "@nestjs/*@10" "cache-manager@5" "@nestjs/cache-manager" -r
echo "Refactor cache.set TTL: seconds → milliseconds"
pnpm up "@nestjs/graphql@11" "@apollo/server@4" -r 2>/dev/null || echo "No GraphQL, skipping"

step "6. Migrate to Nest 11 (Express 5 + config@4)"
pnpm up "@nestjs/*@11" "@nestjs/config@4" "[email protected]" -r
echo "Refactor cfg.get<T> → cfg.getOrThrow<T> for required values"
echo "Verify Express middlewares (passport, custom) for async errors"

step "7. Full validation"
pnpm install --frozen-lockfile
pnpm exec nx run-many -t lint,test,build
pnpm exec nx run-many -t e2e

echo "Migration done. Deploy to staging and observe for 48h."
ts
// scripts/verify-version.ts — quick check after each major
import { execSync } from 'node:child_process';
import { readFileSync } from 'node:fs';

const pkg = JSON.parse(readFileSync('package.json', 'utf8'));
const nestVersion = pkg.dependencies['@nestjs/core'].replace(/^[\^~]/, '');
const major = parseInt(nestVersion.split('.')[0], 10);

const nodeVersion = process.versions.node;
const nodeMajor = parseInt(nodeVersion.split('.')[0], 10);

const expectations: Record<number, number> = { 8: 12, 9: 12, 10: 16, 11: 20 };
const minNode = expectations[major];
if (!minNode) throw new Error(`Unknown Nest major: ${major}`);
if (nodeMajor < minNode) {
  console.error(`Nest ${major} requires Node >= ${minNode}, found ${nodeVersion}`);
  process.exit(1);
}

// Verify deps versions alignment
const requiredCompanion: Record<number, Record<string, RegExp>> = {
  9:  { 'typeorm': /^0\.3/, '@nestjs/typeorm': /^9/ },
  10: { 'cache-manager': /^5/, '@nestjs/cache-manager': /^[2-9]/ },
  11: { '@nestjs/config': /^[4-9]/, 'typescript': /^5/ },
};
for (const [dep, re] of Object.entries(requiredCompanion[major] ?? {})) {
  const v = (pkg.dependencies?.[dep] ?? pkg.devDependencies?.[dep] ?? '').replace(/^[\^~]/, '');
  if (!re.test(v)) {
    console.error(`Companion mismatch: ${dep}=${v}, expected ${re}`);
    process.exit(1);
  }
}

console.log(`OK Nest ${major} on Node ${nodeVersion}, companions aligned`);
yaml
# .github/workflows/migration-pr.yml — gate each migration PR
name: migration-checks
on: { pull_request: { paths: ['package.json', 'pnpm-lock.yaml'] } }
jobs:
  verify:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v3
      - uses: actions/setup-node@v4
        with: { node-version-file: .nvmrc }
      - run: pnpm install --frozen-lockfile
      - run: pnpm tsx scripts/verify-version.ts
      - run: pnpm exec nx affected -t lint,test,build,type-check --base=origin/main
      - run: pnpm exec nx affected -t e2e --base=origin/main
  rollback-plan:
    runs-on: ubuntu-latest
    if: contains(github.event.pull_request.labels.*.name, 'migration')
    steps:
      - run: |
          echo "## Rollback plan" >> $GITHUB_STEP_SUMMARY
          echo "1. \`git revert ${{ github.event.pull_request.head.sha }}\`" >> $GITHUB_STEP_SUMMARY
          echo "2. \`pnpm install --frozen-lockfile\`" >> $GITHUB_STEP_SUMMARY
          echo "3. Redeploy previous image tag from registry" >> $GITHUB_STEP_SUMMARY

Effets concrets : la migration totale (Nest 8 → 11) prend 6 PRs étalées sur 5–8 semaines selon la disponibilité de l'équipe. Chaque PR est testée en staging 48h avant prod. Le script verify-version.ts empêche un merge accidentel d'une version Nest incompatible avec la version Node configurée. Le label migration déclenche l'affichage automatique du rollback plan dans la PR — l'équipe d'astreinte sait quoi faire si ça part en vrille.

🔁 Quand utiliser / éviter

Migrer Nest n'est jamais "drop-in", mais le risque diffère :

  • Nest 7 → 8 : risque faible. RxJS 7 codemod, Swagger renaming mécanique.
  • Nest 8 → 9 + TypeORM 0.2 → 0.3 : risque élevé. Détacher la migration TypeORM en PR séparée si possible.
  • Nest 9 → 10 : risque moyen. cache-manager v5 (TTL en ms) + Node 18 mini. Apollo v4 si GraphQL.
  • Nest 10 → 11 : risque faible. Node 20 mini, Express 5, @nestjs/config v4 (getOrThrow).
  • Évite de migrer plusieurs majors en même temps. Évite de migrer Nest + ORM + node major dans la même PR. Évite de migrer sans coverage > 60% sur les chemins critiques.

🏋️ Exercices

Ces exercices vont de "implémente" à "casse-le puis répare-le". Fais-les sur un repo jouet d'abord, idéalement un Nx monorepo à 2 apps pour sentir le nx affected.

Exercice 1 — Le détecteur de version incompatible (implémente)

Objectif : écrire un script CI qui refuse un merge si la version Nest est incompatible avec la version Node configurée OU si une dep companion (TypeORM, cache-manager, config) n'est pas alignée.

Pars de scripts/verify-version.ts (section "Exemple end-to-end") et durcis-le : lis la version Node réellement utilisée depuis .nvmrc ET engines.node du package.json, vérifie qu'elles concordent, et fail si l'une est sous le minimum du major Nest courant.

Indice / Solution

Compare trois sources de vérité (.nvmrc, package.json#engines.node, process.versions.node) ; toute divergence est un bug de config CI. Parse engines.node avec semver.minVersion(). Mappe { 8:12, 9:12, 10:16, 11:20 } → minNode. Le piège : engines.node est souvent un range (>=18 <21), donc semver.minVersion(range) plutôt qu'un parseInt. Échoue avec un exit code non-zéro et un message actionnable (quelle source est fautive).

Exercice 2 — Le codemod TypeORM @EntityRepository (implémente, niveau dur)

Objectif : écrire un codemod ts-morph qui transforme automatiquement les @EntityRepository(X) class extends Repository<X> (TypeORM 0.2) vers le pattern injecté @InjectDataSource() (0.3), et met à jour tous les sites d'injection.

Indice / Solution

ts-morph : project.getSourceFiles(), trouve les classes décorées @EntityRepository, extrais l'entité depuis l'argument du décorateur. Génère le nouveau pattern (constructeur @InjectDataSource() private ds: DataSource, getter repo). Le vrai travail est de réécrire les consommateurs : tout @InjectRepository(UsersRepository) doit devenir @Inject(UsersRepository). Un sed naïf rate les cas où le repo custom est utilisé comme Repository<X> standard. Teste le codemod avec un snapshot avant/après. Solution acceptable : codemod qui couvre 90% + un rapport listant les 10% à faire main.

Exercice 3 — Le piège du TTL silencieux (casse-le puis répare-le)

Objectif : reproduire le bug sémantique cache-manager v4→v5, l'observer, puis le corriger sans régression.

  1. Monte un service avec cache-manager@4 qui fait cache.set(key, val, { ttl: 300 }) (5 min).
  2. Bump à v5 sans toucher au code : cache.set(key, val, 300) est maintenant interprété comme 300 ms.
  3. Écris un test qui prouve la régression : la valeur expire en 0.3s au lieu de 5min. Mesure le hit ratio.
  4. Corrige proprement (helper typé qui force l'unité) et ajoute un garde-fou.
Indice / Solution

Le test de régression : set, await sleep(1000), get → doit être undefined en v5 (bug), defined après fix. Garde-fou staff : interdis l'appel brut. Crée un wrapper cacheSet(key, val, ttl: Duration)Duration est un branded type (type Ms = number & { __brand: 'ms' }) construit via seconds(300) / minutes(5). Le compilateur refuse alors un number nu. Bonus : une règle ESLint no-restricted-syntax qui bannit .set( direct sur le cache.

Exercice 4 — Express 5 et l'erreur qui ne remontait pas (casse-le puis répare-le)

Objectif : exhiber le changement de propagation d'erreurs async middleware entre Express 4 et 5, puis sécuriser le code.

  1. En Express 4 (Nest 10), écris un middleware async qui throw sans try/catch ni next(err) — observe qu'il hang (timeout) au lieu de renvoyer une 500.
  2. Bump Express 5 (Nest 11) — la même erreur remonte maintenant à l'ExceptionFilter.
  3. Maintenant trouve le piège inverse : un middleware tiers (genre vieux passport-XXX) qui comptait sur l'ancien comportement et avale l'erreur.
Indice / Solution

En Express 4, une rejection async non gérée ne va jamais à next() → la requête pend jusqu'au timeout du LB. En Express 5, c'est propagé. Le piège inverse : du code qui faisait .catch(() => {}) silencieux pour "absorber" l'erreur passe encore, mais du code qui s'appuyait sur le hang (rare, smelly) casse. Fix : audite tout middleware Express tiers, wrappe les non-compatibles dans un adaptateur (req, res, next) => Promise.resolve(handler(req, res)).catch(next). Vérifie via un test e2e qui assert le status code et le temps de réponse (< timeout).

Exercice 5 — Migration zéro-downtime avec contract testing (production-grade)

Objectif : migrer un service Nest consommé par un front Angular sans casser un seul client, garanti par un test de contrat.

Mets en place un snapshot OpenAPI en CI + un Pact consumer-driven entre l'Angular et le Nest. Fais une migration majeure et prouve, avant déploiement, qu'aucun champ de réponse n'a changé de shape (sérialisation class-transformer, req.params null-prototype, dates).

Indice / Solution

Génère openapi.json via SwaggerModule.createDocument en CI, diff contre un snapshot commité avec une whitelist de changements additifs autorisés (ajout de champ optionnel = OK ; rename/removal = fail). Côté Pact : l'Angular publie ses attentes (pact), le Nest les vérifie en CI (@pact-foundation/pact). Le piège réel à attraper : Express 5 sérialise un req.params null-prototype object différemment → un champ qui passait {} devient null dans un payload. Sans contract test, le front casse en prod silencieusement.

🎤 En entretien

Q : Tu dois migrer un projet de Nest 7 à Nest 11. Quel est le vrai risque, et comment tu séquences ? Le risque n'est pas Nest core (très stable) mais les deps tierces qui bumpent en parallèle — surtout TypeORM 0.2→0.3 (architectural) et cache-manager v4→v5 (sémantique, TTL en ms). Je migre un major à la fois, dans sa propre PR mergée et déployée, pour garder un espace de debug O(N) au lieu du produit cartésien. Je détache TypeORM en PR autonome même si c'est dans le même major Nest. Prérequis : coverage > 60% sur les chemins critiques avant de commencer.

Q : Quelle catégorie de breaking change est la plus dangereuse, et pourquoi ? Les breaking changes sémantiques : le code compile, les types passent, les tests verts, mais le comportement runtime change. Exemple canonique : cache-manager où { ttl: 60 } (60s) devient 60 (60ms) — le cache expire instantanément, le hit ratio s'effondre, et rien ne te le signale en CI. Contrairement aux breaking mécaniques (renommage, attrapables par le compilateur) ou architecturaux (visibles, ils ne compilent pas), les sémantiques exigent une observabilité différentielle en prod pour être détectés.

Q : Staging passe vert pendant 48h. Pourquoi ce n'est pas suffisant pour un service à fort trafic ? Staging n'a ni le volume, ni la distribution réelle du trafic, ni les patterns de cache chauds de la prod. Une régression de p99 ou un memory leak sous charge ne se révèle qu'au volume de prod. La vraie garantie est un déploiement canary (5% → 25% → 100%) avec analyse automatique : rollback si error-rate ou p99 dévie du baseline. Je capture une baseline de métriques labellisées app_version avant le merge pour comparer côte à côte.

Q : Ton service Nest est consommé par 3 équipes. Comment garantir qu'une migration ne casse aucun client ? Tests unitaires et e2e ne protègent que mon code, pas les attentes des consommateurs. Je verrouille le contrat : snapshot OpenAPI diffé en CI (rename/removal de champ = fail, ajout optionnel = OK) plus du contract testing consumer-driven (Pact) où chaque consommateur publie ses attentes que mon service vérifie en CI. Une migration qui change le shape de réponse sans bump de version d'API est un incident, pas une migration.

🔗 Liens

Bibliothèque tech perso — Achref