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
| Concept | Spring | NestJS | Où 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 statut | ResponseEntity, @ResponseStatus | Objet retourné + @HttpCode() (@Res() à éviter) | src/projects/projects.controller.ts (DELETE → 204) |
| DTO + validation | Bean Validation (@Valid, @NotNull) | class-validator + ValidationPipe | src/projects/dto/project.dto.ts, src/app.module.ts (APP_PIPE) |
| Sérialisation | Jackson | class-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. LeValidationPipeglobal 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é commeParseUUIDPipe(validation). - La query string ne contient que des chaînes.
@Type(() => Number)convertit les nombres, et?archived=falseexige un@Transformexplicite, car la chaîne"false"est vraie en JavaScript (pagination). - La sérialisation n'est pas automatique. Jackson s'applique à toute réponse ;
@Excludene s'applique que siClassSerializerInterceptorest 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 pasResponseEntity. Il fait passer le handler en mode manuel : intercepteurs et sérialisation ne s'appliquent plus (piège).
Injection et modules
| Concept | Spring | NestJS | Où dans TaskFlow |
|---|---|---|---|
| Démarrage | SpringApplication.run | NestFactory.create | src/main.ts |
| Bean de service | @Service, @Component | @Injectable() | src/projects/projects.service.ts |
| Injection | Constructeur ou @Autowired | Injection par constructeur (types TS + metadata) | src/tasks/tasks.service.ts (par type, @InjectRepository(Task), @Inject(CLOCK)) |
| Conteneur / contexte | ApplicationContext, component scan | @Module({ providers, controllers, imports, exports }) | src/users/users.module.ts, src/app.module.ts |
| Scopes de bean | singleton, request | Scope.DEFAULT (singleton), Scope.REQUEST, Scope.TRANSIENT | Tous les providers de TaskFlow sont des singletons (scope par défaut) |
| Bean fabriqué | @Configuration + @Bean | useFactory | src/projects/projects.module.ts (provider PROJECTS_STORE) |
| Bean conditionnel | @Profile, @ConditionalOnProperty | Provider choisi selon la configuration, ConditionalModule | src/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 configurable | Auto-configuration + application.yml | Modules 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
providersn'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>devientRepository, d'où@InjectRepository(Task)(différences). - Pas de proxy de scope. Spring injecte un bean
requestdans un singleton via un proxy. Dans Nest, un providerREQUESTrend 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
| Concept | Spring | NestJS | Où dans TaskFlow |
|---|---|---|---|
| Repository | JpaRepository<T, ID> | Repository<T> TypeORM, @InjectRepository(T) | src/projects/typeorm-projects.store.ts, src/tasks/tasks.service.ts |
| Requête dérivée | findByNameContaining(...) | find({ where: { name: ILike(...) } }) ou QueryBuilder | src/projects/typeorm-projects.store.ts (findPage) |
| Pagination | Page<T>, Pageable | findAndCount + skip / take | src/projects/typeorm-projects.store.ts, src/common/dto/pagination.dto.ts |
| Entité | @Entity, @Id, @Column | @Entity, @PrimaryGeneratedColumn, @Column | src/projects/entities/project.entity.ts |
| Relations | @OneToMany, @ManyToOne | Mêmes décorateurs TypeORM | src/tasks/entities/task.entity.ts, src/projects/entities/project-member.entity.ts |
| Transaction | @Transactional | DataSource.transaction() ou QueryRunner (pas d'équivalent déclaratif natif) | src/projects/typeorm-projects.store.ts (create, archive) |
| Migrations | Flyway, Liquibase | Migrations TypeORM (CLI) | src/database/migrations/, src/database/data-source.ts |
| Schéma automatique | spring.jpa.hibernate.ddl-auto | synchronize: true (développement uniquement) | src/database/postgres-options.ts (synchronize: false) |
| Données de développement | data.sql, CommandLineRunner | Script de seed | src/database/seed.ts |
Là où l'analogie s'arrête :
@Transactionaln'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 lemanagerreçu. Un repository injecté, lui, s'exécute hors de la transaction (transactions). Des bibliothèques tierces reconstruisent un décorateur avecAsyncLocalStorage; 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()ouupdate(). - Pas de chargement paresseux transparent. Une relation non demandée (
relations, jointure) vautundefined, sansLazyInitializationExceptionpour vous prévenir (chargement). - Le type de colonne s'écrit. Pour
description: string | null, le compilateur n'émet queObjectcomme type : TypeORM ne peut rien en déduire, d'où lestype: 'text',type: 'varchar'explicites des entités TaskFlow (piège des métadonnées).
Pipeline de requête
| Concept | Spring | NestJS | Où dans TaskFlow |
|---|---|---|---|
| Ordre d'exécution | Filtres servlet → DispatcherServlet → intercepteurs → contrôleur | Middleware → Guards → Interceptors → Pipes → Handler → Interceptors → Exception Filters | src/app.module.ts |
| Gestion d'exceptions | @ControllerAdvice + @ExceptionHandler | ExceptionFilter avec @Catch(), enregistré via APP_FILTER | src/common/filters/all-exceptions.filter.ts |
| Exceptions HTTP | ResponseStatusException | HttpException, NotFoundException, etc. | src/projects/projects.service.ts, src/auth/auth.service.ts (ConflictException) |
| Filtre servlet | Filter | Middleware | src/common/middleware/correlation-id.middleware.ts |
| Intercepteur | HandlerInterceptor / AOP @Around | NestInterceptor (RxJS) | src/common/interceptors/logging.interceptor.ts |
| Conversion et validation de paramètres | Converter, @InitBinder | Pipe (ParseIntPipe, ParseUUIDPipe) | src/tasks/tasks.controller.ts (ParseUUIDPipe, ParseEnumPipe) |
Là où l'analogie s'arrête :
- Les intercepteurs n'entourent que les handlers. Un
@AroundSpring 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
Filterservlet, 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) surnext.handle(), pas après unproceed().
Configuration et cycle de vie
| Concept | Spring | NestJS | Où dans TaskFlow |
|---|---|---|---|
| Configuration | application.yml, @Value, @ConfigurationProperties | @nestjs/config (ConfigModule, ConfigService) | src/app.module.ts, src/config/ |
| Propriétés typées | @ConfigurationProperties | registerAs + config typée | src/config/configuration.ts |
| Validation au démarrage | @Validated sur les propriétés | Option validate (class-validator ou Joi) | src/config/env.validation.ts |
| Profils | spring.profiles.active | NODE_ENV + fichiers .env.* | src/app.module.ts (envFilePath), .env.example |
| Logging | SLF4J + Logback | Logger Nest, nestjs-pino | src/app.module.ts (LoggerModule) |
| Cycle de vie | @PostConstruct, @PreDestroy | OnModuleInit, 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_ENVn'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}viaenvFilePathet n'accepte quedevelopment,testetproduction.- Les arrêts propres sont à activer. Spring Boot enregistre un hook d'arrêt de la JVM ; Nest n'appelle
OnModuleDestroyetOnApplicationShutdownsur un signal qu'aprèsapp.enableShutdownHooks().
Sécurité
| Concept | Spring | NestJS | Où dans TaskFlow |
|---|---|---|---|
| Autorisation | Spring Security, @PreAuthorize | Guard (CanActivate), @UseGuards, @Roles custom | src/auth/guards.ts, src/common/decorators/auth.decorators.ts |
| Chaîne de sécurité | SecurityFilterChain | Guards globaux (APP_GUARD) | src/app.module.ts |
| Authentification JWT | Resource server JWT de Spring Security | @nestjs/passport + passport-jwt, @nestjs/jwt | src/auth/jwt.strategy.ts, src/auth/auth.module.ts |
| Route publique | permitAll() | Décorateur @Public() lu par le guard global | src/common/decorators/auth.decorators.ts, src/auth/auth.controller.ts |
| Utilisateur courant | SecurityContextHolder, @AuthenticationPrincipal | request.user, @CurrentUser() | src/common/decorators/auth.decorators.ts |
| Hash de mot de passe | PasswordEncoder | argon2 (ou bcrypt) | src/auth/auth.service.ts |
| En-têtes, CORS, limitation de débit | headers(), cors() de Spring Security ; pas de limitation de débit standard | helmet(), enableCors(), @nestjs/throttler | src/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
@PreAuthorizesur 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
| Concept | Spring | NestJS | Où dans TaskFlow |
|---|---|---|---|
| Asynchrone | @Async, CompletableFuture, WebFlux | async/await, Promise, RxJS | Partout ; src/demo/event-loop.controller.ts pour l'event loop |
| Événements | ApplicationEventPublisher, @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 file | Spring Batch, JMS | @nestjs/bullmq (Redis) | src/notifications/notifications.queue.ts |
Là où l'analogie s'arrête :
asyncne crée pas de thread.@Asyncexécute la méthode sur un autre thread ; une fonctionasyncs'exécute sur l'unique thread JavaScript et rend la main à chaqueawait. 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
@Scheduledsans 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
| Concept | Spring | NestJS | Où dans TaskFlow |
|---|---|---|---|
| Tests | JUnit, Mockito, MockMvc, @SpringBootTest | Jest, Test.createTestingModule, supertest | src/**/*.spec.ts, test/app.e2e-spec.ts |
| Mocks | Mockito (mock(), when()) | jest.fn(), jest.Mocked<T> | src/tasks/tasks.service.spec.ts |
| Mock de repository | @MockitoBean (ex-@MockBean) sur un JpaRepository | getRepositoryToken(Entity) + useValue | src/tasks/tasks.service.spec.ts |
| Test d'application | @SpringBootTest + MockMvc | Test.createTestingModule + supertest | test/app.e2e-spec.ts |
| Base réelle | @DataJpaTest, Testcontainers | Test d'intégration avec vraie base (Testcontainers ou service Compose) | Service db-test de docker-compose.yml |
| Couverture | JaCoCo | jest --coverage + coverageThreshold | jest.config.ts |
Là où l'analogie s'arrête :
- Un module de test ne scanne rien.
Test.createTestingModulene 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 dansmain.ts, que les tests n'exécutent pas (e2e).
Outillage, documentation et production
| Concept | Spring | NestJS | Où dans TaskFlow |
|---|---|---|---|
| Build / dépendances | Maven, Gradle | npm ou pnpm, package.json | package.json, package-lock.json |
| Lombok | @Getter, @Builder | Inutile (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 projet | Spring Initializr | Nest CLI (nest new, nest g) | nest-cli.json |
| Rechargement à chaud | Spring Boot DevTools | nest start --watch | package.json (npm run start:dev) |
| Actuator | /actuator/health | @nestjs/terminus | src/health/health.controller.ts |
| OpenAPI | springdoc-openapi | @nestjs/swagger | src/app.setup.ts (setupSwagger), nest-cli.json (plugin) |
| Image Docker | Jib, spring-boot:build-image | Dockerfile multi-stage | Dockerfile |
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/swaggerdéclaré dansnest-cli.json(Swagger). Il ne lit que les fichiers*.dto.tset*.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,envoumetricsd'Actuator.