Skip to content

Correspondances Spring ↔ NestJS ​

Cette page reprend toute la table de correspondance du plan, classée par thème et complétée par les correspondances données jour par jour. Le nom du concept mène à la section du chapitre qui l'explique ; la colonne « Où dans TaskFlow » indique le fichier où le voir en action (chemins relatifs au dossier examples/). Sous chaque table, une courte liste signale où l'analogie avec Spring cesse de tenir : c'est là que se trouvent la plupart des erreurs d'un développeur Java.

Web : contrôleurs, requêtes, réponses ​

ConceptSpringNestJSOù dans TaskFlow
Contrôleur REST@RestController@Controller('projects')src/projects/projects.controller.ts
Mapping HTTP@GetMapping, @PostMapping@Get(), @Post()src/projects/projects.controller.ts, src/tasks/tasks.controller.ts
Corps de requête@RequestBody@Body()src/projects/projects.controller.ts
Paramètre de chemin@PathVariable@Param('id')src/projects/projects.controller.ts (avec ParseUUIDPipe)
Paramètre de requête@RequestParam@Query()src/projects/dto/project.dto.ts (ProjectQueryDto), src/tasks/tasks.controller.ts (@Query('status'))
En-têtes@RequestHeader@Headers()Pas de @Headers() : l'en-tête x-correlation-id est lu dans src/common/correlation-id.ts
Code de statutResponseEntity, @ResponseStatusObjet retourné + @HttpCode() (@Res() à éviter)src/projects/projects.controller.ts (DELETE → 204)
DTO + validationBean Validation (@Valid, @NotNull)class-validator + ValidationPipesrc/projects/dto/project.dto.ts, src/app.module.ts (APP_PIPE)
SérialisationJacksonclass-transformer (@Exclude, @Expose)src/users/entities/user.entity.ts (@Exclude), src/app.module.ts (ClassSerializerInterceptor)

Là où l'analogie s'arrête :

  • Pas de @Valid, mais pas de validation implicite non plus. Le ValidationPipe global valide chaque paramètre typé par une classe DTO. Le type TypeScript seul ne vérifie rien à l'exécution, et un paramètre primitif (@Param('id') id: string) n'est pas validé : il faut un pipe dédié comme ParseUUIDPipe (validation).
  • La query string ne contient que des chaînes. @Type(() => Number) convertit les nombres, et ?archived=false exige un @Transform explicite, car la chaîne "false" est vraie en JavaScript (pagination).
  • La sérialisation n'est pas automatique. Jackson s'applique à toute réponse ; @Exclude ne s'applique que si ClassSerializerInterceptor est actif et que la valeur renvoyée est une instance de la classe. Un objet recopié ({ ...user }) perd ses métadonnées et donc l'exclusion (piège des entités brutes).
  • @Res() n'est pas ResponseEntity. Il fait passer le handler en mode manuel : intercepteurs et sérialisation ne s'appliquent plus (piège).

Injection et modules ​

ConceptSpringNestJSOù dans TaskFlow
DémarrageSpringApplication.runNestFactory.createsrc/main.ts
Bean de service@Service, @Component@Injectable()src/projects/projects.service.ts
InjectionConstructeur ou @AutowiredInjection par constructeur (types TS + metadata)src/tasks/tasks.service.ts (par type, @InjectRepository(Task), @Inject(CLOCK))
Conteneur / contexteApplicationContext, component scan@Module({ providers, controllers, imports, exports })src/users/users.module.ts, src/app.module.ts
Scopes de beansingleton, requestScope.DEFAULT (singleton), Scope.REQUEST, Scope.TRANSIENTTous les providers de TaskFlow sont des singletons (scope par défaut)
Bean fabriqué@Configuration + @BeanuseFactorysrc/projects/projects.module.ts (provider PROJECTS_STORE)
Bean conditionnel@Profile, @ConditionalOnPropertyProvider choisi selon la configuration, ConditionalModulesrc/projects/projects.module.ts, src/notifications/notifications.module.ts
Qualificateur@Qualifier("nom")Token personnalisé : @Inject(PROJECTS_STORE)src/projects/projects.store.ts, src/common/common.module.ts (CLOCK)
Module configurableAuto-configuration + application.ymlModules dynamiques forRoot() / forRootAsync()src/database/database.module.ts, src/auth/auth.module.ts

Là où l'analogie s'arrête :

  • Pas de component scan. Un provider qui n'est pas listé dans providers n'existe pas, et un provider non exporté reste invisible des autres modules. L'erreur arrive au démarrage (« Nest can't resolve dependencies of … »), pas à la compilation (modules).
  • Les types disparaissent à l'exécution. Une interface ne peut pas servir de clé d'injection : il faut un token. Les génériques aussi sont effacés : Repository<Task> devient Repository, d'où @InjectRepository(Task) (différences).
  • Pas de proxy de scope. Spring injecte un bean request dans un singleton via un proxy. Dans Nest, un provider REQUEST rend request-scoped tous ceux qui en dépendent, jusqu'au contrôleur : toute la chaîne est recréée à chaque requête (scopes).
  • Les cycles se paient. forwardRef() existe, mais un cycle entre modules signale d'abord un découpage à revoir (piège).

Données ​

ConceptSpringNestJSOù dans TaskFlow
RepositoryJpaRepository<T, ID>Repository<T> TypeORM, @InjectRepository(T)src/projects/typeorm-projects.store.ts, src/tasks/tasks.service.ts
Requête dérivéefindByNameContaining(...)find({ where: { name: ILike(...) } }) ou QueryBuildersrc/projects/typeorm-projects.store.ts (findPage)
PaginationPage<T>, PageablefindAndCount + skip / takesrc/projects/typeorm-projects.store.ts, src/common/dto/pagination.dto.ts
Entité@Entity, @Id, @Column@Entity, @PrimaryGeneratedColumn, @Columnsrc/projects/entities/project.entity.ts
Relations@OneToMany, @ManyToOneMêmes décorateurs TypeORMsrc/tasks/entities/task.entity.ts, src/projects/entities/project-member.entity.ts
Transaction@TransactionalDataSource.transaction() ou QueryRunner (pas d'équivalent déclaratif natif)src/projects/typeorm-projects.store.ts (create, archive)
MigrationsFlyway, LiquibaseMigrations TypeORM (CLI)src/database/migrations/, src/database/data-source.ts
Schéma automatiquespring.jpa.hibernate.ddl-autosynchronize: true (développement uniquement)src/database/postgres-options.ts (synchronize: false)
Données de développementdata.sql, CommandLineRunnerScript de seedsrc/database/seed.ts

Là où l'analogie s'arrête :

  • @Transactional n'a pas d'équivalent déclaratif natif. Spring rattache la transaction au thread courant. Node sert toutes les requêtes sur un seul thread : la transaction est donc un callback, et chaque requête qui doit en faire partie passe par le manager reçu. Un repository injecté, lui, s'exécute hors de la transaction (transactions). Des bibliothèques tierces reconstruisent un décorateur avec AsyncLocalStorage ; TaskFlow s'en tient à l'API TypeORM.
  • Pas de dirty checking. Modifier une entité chargée ne déclenche aucune écriture à la fin de la transaction : il faut appeler save() ou update().
  • Pas de chargement paresseux transparent. Une relation non demandée (relations, jointure) vaut undefined, sans LazyInitializationException pour vous prévenir (chargement).
  • Le type de colonne s'écrit. Pour description: string | null, le compilateur n'émet que Object comme type : TypeORM ne peut rien en déduire, d'où les type: 'text', type: 'varchar' explicites des entités TaskFlow (piège des métadonnées).

Pipeline de requête ​

ConceptSpringNestJSOù dans TaskFlow
Ordre d'exécutionFiltres servlet → DispatcherServlet → intercepteurs → contrôleurMiddleware → Guards → Interceptors → Pipes → Handler → Interceptors → Exception Filterssrc/app.module.ts
Gestion d'exceptions@ControllerAdvice + @ExceptionHandlerExceptionFilter avec @Catch(), enregistré via APP_FILTERsrc/common/filters/all-exceptions.filter.ts
Exceptions HTTPResponseStatusExceptionHttpException, NotFoundException, etc.src/projects/projects.service.ts, src/auth/auth.service.ts (ConflictException)
Filtre servletFilterMiddlewaresrc/common/middleware/correlation-id.middleware.ts
IntercepteurHandlerInterceptor / AOP @AroundNestInterceptor (RxJS)src/common/interceptors/logging.interceptor.ts
Conversion et validation de paramètresConverter, @InitBinderPipe (ParseIntPipe, ParseUUIDPipe)src/tasks/tasks.controller.ts (ParseUUIDPipe, ParseEnumPipe)

Là où l'analogie s'arrête :

  • Les intercepteurs n'entourent que les handlers. Un @Around Spring peut cibler n'importe quelle méthode de bean ; un intercepteur Nest ne s'applique qu'aux routes. Les services ne sont pas proxifiés.
  • Le middleware ne connaît pas le handler. Comme un Filter servlet, il voit la requête brute, sans savoir quel contrôleur va répondre. Pour décider selon la route ou ses métadonnées, utilisez un guard ou un intercepteur (ordre d'exécution).
  • Un intercepteur manipule un Observable. Le code après le handler s'écrit avec les opérateurs RxJS (tap, map, catchError) sur next.handle(), pas après un proceed().

Configuration et cycle de vie ​

ConceptSpringNestJSOù dans TaskFlow
Configurationapplication.yml, @Value, @ConfigurationProperties@nestjs/config (ConfigModule, ConfigService)src/app.module.ts, src/config/
Propriétés typées@ConfigurationPropertiesregisterAs + config typéesrc/config/configuration.ts
Validation au démarrage@Validated sur les propriétésOption validate (class-validator ou Joi)src/config/env.validation.ts
Profilsspring.profiles.activeNODE_ENV + fichiers .env.*src/app.module.ts (envFilePath), .env.example
LoggingSLF4J + LogbackLogger Nest, nestjs-pinosrc/app.module.ts (LoggerModule)
Cycle de vie@PostConstruct, @PreDestroyOnModuleInit, OnModuleDestroy (+ enableShutdownHooks())src/app.setup.ts (enableShutdownHooks())

Là où l'analogie s'arrête :

  • Tout arrive en chaîne. Les variables d'environnement sont des chaînes ; la conversion et la validation sont explicites (validateEnv). Écrivez le type des propriétés (PORT: number = 3000), sinon la conversion implicite ne se fait pas (piège).
  • NODE_ENV n'est pas un profil Spring. Aucun fichier n'est fusionné automatiquement et aucun bean n'est activé par profil : TaskFlow choisit le fichier .env.${NODE_ENV} via envFilePath et n'accepte que development, test et production.
  • Les arrêts propres sont à activer. Spring Boot enregistre un hook d'arrêt de la JVM ; Nest n'appelle OnModuleDestroy et OnApplicationShutdown sur un signal qu'après app.enableShutdownHooks().

Sécurité ​

ConceptSpringNestJSOù dans TaskFlow
AutorisationSpring Security, @PreAuthorizeGuard (CanActivate), @UseGuards, @Roles customsrc/auth/guards.ts, src/common/decorators/auth.decorators.ts
Chaîne de sécuritéSecurityFilterChainGuards globaux (APP_GUARD)src/app.module.ts
Authentification JWTResource server JWT de Spring Security@nestjs/passport + passport-jwt, @nestjs/jwtsrc/auth/jwt.strategy.ts, src/auth/auth.module.ts
Route publiquepermitAll()Décorateur @Public() lu par le guard globalsrc/common/decorators/auth.decorators.ts, src/auth/auth.controller.ts
Utilisateur courantSecurityContextHolder, @AuthenticationPrincipalrequest.user, @CurrentUser()src/common/decorators/auth.decorators.ts
Hash de mot de passePasswordEncoderargon2 (ou bcrypt)src/auth/auth.service.ts
En-têtes, CORS, limitation de débitheaders(), cors() de Spring Security ; pas de limitation de débit standardhelmet(), enableCors(), @nestjs/throttlersrc/app.setup.ts, src/auth/auth.controller.ts (@Throttle)

Là où l'analogie s'arrête :

  • Il n'y a pas de framework de sécurité intégré. Vous assemblez une stratégie Passport, des guards et des décorateurs. La sécurité par défaut n'existe que si vous enregistrez un guard global, comme TaskFlow avec JwtAuthGuard (guard global).
  • Les guards protègent des routes, pas des méthodes de service. Il n'y a pas d'équivalent de @PreAuthorize sur un @Service : les règles d'accès métier (membre du projet) vivent dans le service (membres).
  • Le rôle vient du token. Un changement de rôle ne s'applique qu'à la connexion suivante (token de 15 minutes dans TaskFlow).

Asynchrone, événements, planification ​

ConceptSpringNestJSOù dans TaskFlow
Asynchrone@Async, CompletableFuture, WebFluxasync/await, Promise, RxJSPartout ; src/demo/event-loop.controller.ts pour l'event loop
ÉvénementsApplicationEventPublisher, @EventListener@nestjs/event-emitter (EventEmitter2, @OnEvent)src/tasks/tasks.service.ts (emit), src/notifications/notifications.listener.ts
Planification@Scheduled@nestjs/schedule (@Cron, @Interval)src/tasks/tasks.scheduler.ts
Jobs en fileSpring Batch, JMS@nestjs/bullmq (Redis)src/notifications/notifications.queue.ts

Là où l'analogie s'arrête :

  • async ne crée pas de thread. @Async exécute la méthode sur un autre thread ; une fonction async s'exécute sur l'unique thread JavaScript et rend la main à chaque await. Un calcul lourd bloque toutes les requêtes (event loop).
  • Une erreur de listener ne remonte pas. Avec @OnEvent(..., { async: true }), l'exception n'atteint jamais le code qui a émis l'événement : attrapez-la et journalisez-la dans le listener (erreurs asynchrones). Il n'y a pas non plus de @TransactionalEventListener : TaskFlow émet après la sauvegarde.
  • Le cron tourne sur chaque instance, comme un @Scheduled sans verrou (ShedLock côté Spring). Avec plusieurs réplicas, passez par une file ou un verrou (cron).
  • BullMQ est une file de jobs Redis (retries, backoff, délais), plus proche d'une file JMS avec redelivery que de Spring Batch, qui découpe un traitement en étapes et en lots.

Tests ​

ConceptSpringNestJSOù dans TaskFlow
TestsJUnit, Mockito, MockMvc, @SpringBootTestJest, Test.createTestingModule, supertestsrc/**/*.spec.ts, test/app.e2e-spec.ts
MocksMockito (mock(), when())jest.fn(), jest.Mocked<T>src/tasks/tasks.service.spec.ts
Mock de repository@MockitoBean (ex-@MockBean) sur un JpaRepositorygetRepositoryToken(Entity) + useValuesrc/tasks/tasks.service.spec.ts
Test d'application@SpringBootTest + MockMvcTest.createTestingModule + supertesttest/app.e2e-spec.ts
Base réelle@DataJpaTest, TestcontainersTest d'intégration avec vraie base (Testcontainers ou service Compose)Service db-test de docker-compose.yml
CouvertureJaCoCojest --coverage + coverageThresholdjest.config.ts

Là où l'analogie s'arrête :

  • Un module de test ne scanne rien. Test.createTestingModule ne contient que les providers listés : c'est à la fois plus rapide qu'un contexte Spring et plus explicite.
  • Un test qui ne démarre pas Nest charge lui-même reflect-metadata (piège).
  • supertest envoie de vraies requêtes HTTP au serveur de l'application : c'est plus proche de @SpringBootTest(webEnvironment = RANDOM_PORT) que de MockMvc, qui simule la couche servlet.
  • Les providers globaux déclarés via APP_* s'appliquent dans les e2e, contrairement à app.useGlobalPipes() écrit dans main.ts, que les tests n'exécutent pas (e2e).

Outillage, documentation et production ​

ConceptSpringNestJSOù dans TaskFlow
Build / dépendancesMaven, Gradlenpm ou pnpm, package.jsonpackage.json, package-lock.json
Lombok@Getter, @BuilderInutile (TS, propriétés publiques)Entités et DTO à propriétés publiques ; propriétés de paramètre dans les constructeurs
Générateur de projetSpring InitializrNest CLI (nest new, nest g)nest-cli.json
Rechargement à chaudSpring Boot DevToolsnest start --watchpackage.json (npm run start:dev)
Actuator/actuator/health@nestjs/terminussrc/health/health.controller.ts
OpenAPIspringdoc-openapi@nestjs/swaggersrc/app.setup.ts (setupSwagger), nest-cli.json (plugin)
Image DockerJib, spring-boot:build-imageDockerfile multi-stageDockerfile

Là où l'analogie s'arrête :

  • Pas de cycle de vie de build. Maven enchaîne ses phases ; ici chaque outil a son script (npm run build, npm test, npm run lint) et sa configuration (tsconfig.json, jest.config.ts, nest-cli.json, .oxlintrc.json).
  • Lombok n'a rien à faire. Une classe TypeScript expose des propriétés publiques sans getters, et constructor(private readonly service: ProjectsService) {} déclare et affecte le champ en une ligne.
  • Swagger a besoin d'un plugin de compilation. springdoc lit les types par réflexion à l'exécution ; les types TypeScript n'existent plus à ce moment-là, d'où le plugin @nestjs/swagger déclaré dans nest-cli.json (Swagger). Il ne lit que les fichiers *.dto.ts et *.entity.ts, et ne tient pas compte de @Exclude() : un champ jamais renvoyé se masque dans la documentation avec @ApiHideProperty() (Swagger sur /docs).
  • Terminus ne fait que des health checks. Pas d'équivalent des endpoints info, env ou metrics d'Actuator.