Back-end angularforall.com

- NestJS : architecture modulaire et API REST scalable

Nestjs Node-Js Typescript Api-Rest Typeorm Postgresql Dependency-Injection Swagger Jest Class-Validator Modules Controllers Backend Decorators Architecture
NestJS : architecture modulaire et API REST scalable

Construisez une API NestJS scalable : architecture modulaire, injection de dépendances, controllers, TypeORM PostgreSQL, validation, Swagger et tests Jest.

1. Pourquoi NestJS pour le back-end

NestJS est un framework Node.js progressif construit avec TypeScript et largement inspiré d'Angular. Il propose une architecture modulaire opinion-driven (modules, controllers, providers), un IoC container avec injection de dépendances, et un écosystème de modules officiels (TypeORM, Mongoose, GraphQL, WebSockets, Microservices).

Express vs NestJS : quelle différence concrète ?

Critère Express NestJS
Architecture Libre, à structurer soi-même Modulaire, opinion-driven
TypeScript À configurer manuellement First-class, decorators natifs
Dependency Injection Aucune (manuelle) IoC container intégré
Validation Middleware tiers (joi, zod) ValidationPipe + class-validator
Documentation API Manuelle (Swagger UI à brancher) @nestjs/swagger générée automatiquement
Tests Mocha/Jest à configurer Jest + TestingModule prêt à l'emploi
💡 Quand choisir NestJS ?
  • Équipe Angular qui veut un back-end avec les mêmes patterns (DI, decorators, modules)
  • API d'entreprise avec contrôleurs, services, gestion d'erreurs et logs structurés
  • Architecture évolutive (microservices, GraphQL, WebSockets sans refactoring)
  • Équipe TypeScript déjà autonome (sinon courbe d'apprentissage notable)

Sous le capot : Express ou Fastify ?

NestJS est un méta-framework : il s'appuie par défaut sur Express, mais peut basculer sur Fastify en changeant l'adapter. Vous gardez tout votre code NestJS, seul le HTTP layer change. Fastify donne typiquement +30% de throughput sur les benchmarks JSON pur.

2. Prérequis et installation

Environnement requis

  • Node.js 20 LTS ou supérieur
  • npm 10+ ou pnpm 9+
  • PostgreSQL 15+ (local ou Docker)
  • TypeScript 5+ (installé via le starter NestJS)
  • Postman / Insomnia / Bruno pour tester les endpoints

Installer le CLI et créer un projet


# Installer le CLI globalement
npm install -g @nestjs/cli

# Vérifier la version
nest --version

# Créer un nouveau projet
nest new tasks-api
# → choisir npm ou pnpm

# Lancer en dev (hot reload)
cd tasks-api
npm run start:dev

Le serveur écoute par défaut sur http://localhost:3000. Le CLI génère une structure prête à l'emploi avec main.ts, app.module.ts, app.controller.ts et app.service.ts.

Arborescence recommandée pour une API d'entreprise


tasks-api/
├── src/
│   ├── main.ts                    # Bootstrap de l'application
│   ├── app.module.ts              # Module racine
│   ├── common/                    # Pipes, filters, guards partagés
│   │   ├── filters/
│   │   │   └── http-exception.filter.ts
│   │   ├── guards/
│   │   │   └── jwt-auth.guard.ts
│   │   └── interceptors/
│   │       └── logging.interceptor.ts
│   ├── config/                    # Variables d'env typées
│   │   └── configuration.ts
│   ├── tasks/                     # Module métier "tasks"
│   │   ├── tasks.module.ts
│   │   ├── tasks.controller.ts
│   │   ├── tasks.service.ts
│   │   ├── dto/
│   │   │   ├── create-task.dto.ts
│   │   │   └── update-task.dto.ts
│   │   └── entities/
│   │       └── task.entity.ts
│   └── users/                     # Module "users"
│       └── ...
├── test/
│   └── tasks.e2e-spec.ts          # Tests end-to-end
├── .env
├── .env.example
├── nest-cli.json
├── tsconfig.json
└── package.json
📌 Convention : chaque domaine métier (users, tasks, billing…) est un module dédié avec son controller, son service et ses DTO. C'est la base de la scalabilité NestJS.

3. Architecture modulaire (modules, providers, controllers)

NestJS organise une application autour de trois briques : modules (regroupent une feature), controllers (gèrent les requêtes HTTP), providers (services injectables qui contiennent la logique métier).

Le module racine


// src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { TasksModule } from './tasks/tasks.module';
import { UsersModule } from './users/users.module';

@Module({
  imports: [
    // Charge .env de manière globale et type-safe
    ConfigModule.forRoot({ isGlobal: true }),
    TasksModule,
    UsersModule,
  ],
  controllers: [],
  providers: [],
})
export class AppModule {}

Un module métier


// src/tasks/tasks.module.ts
import { Module } from '@nestjs/common';
import { TasksController } from './tasks.controller';
import { TasksService } from './tasks.service';

@Module({
  // Controllers HTTP exposés par ce module
  controllers: [TasksController],
  // Providers (services) instanciés et injectables
  providers: [TasksService],
  // Exporter le service pour le rendre utilisable dans d'autres modules
  exports: [TasksService],
})
export class TasksModule {}

Un controller


// src/tasks/tasks.controller.ts
import { Controller, Get, Post, Body, Param, Delete } from '@nestjs/common';
import { TasksService } from './tasks.service';
import { CreateTaskDto } from './dto/create-task.dto';

@Controller('tasks') // Toutes les routes sont préfixées par /tasks
export class TasksController {
  // Injection de dépendances par constructeur
  constructor(private readonly tasksService: TasksService) {}

  @Get()
  findAll() {
    // Délégation au service : le controller reste mince
    return this.tasksService.findAll();
  }

  @Get(':id')
  findOne(@Param('id') id: string) {
    return this.tasksService.findOne(id);
  }

  @Post()
  create(@Body() dto: CreateTaskDto) {
    return this.tasksService.create(dto);
  }

  @Delete(':id')
  remove(@Param('id') id: string) {
    return this.tasksService.remove(id);
  }
}
🎯 Règle d'or : les controllers ne contiennent jamais de logique métier. Leur seul rôle est de traduire HTTP en appels de service. Toute la logique vit dans les providers (services, repositories, helpers).

4. Injection de dépendances

L'IoC container de NestJS résout automatiquement le graphe de dépendances au démarrage. Quand un controller demande TasksService dans son constructeur, Nest cherche le provider correspondant dans le module courant ou ses imports.

Service injectable


// src/tasks/tasks.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { randomUUID } from 'crypto';
import { CreateTaskDto } from './dto/create-task.dto';

@Injectable() // Marqueur indispensable : le provider devient injectable
export class TasksService {
  // Stockage en mémoire pour la démo (remplacé par TypeORM en section 6)
  private tasks: Array<{ id: string; title: string; done: boolean }> = [];

  findAll() {
    return this.tasks;
  }

  findOne(id: string) {
    const task = this.tasks.find((t) => t.id === id);
    if (!task) {
      // Exception HTTP traduite automatiquement en 404 par Nest
      throw new NotFoundException(`Task ${id} introuvable`);
    }
    return task;
  }

  create(dto: CreateTaskDto) {
    const task = { id: randomUUID(), title: dto.title, done: false };
    this.tasks.push(task);
    return task;
  }

  remove(id: string) {
    const before = this.tasks.length;
    this.tasks = this.tasks.filter((t) => t.id !== id);
    if (this.tasks.length === before) {
      throw new NotFoundException(`Task ${id} introuvable`);
    }
    return { deleted: id };
  }
}

Providers personnalisés (custom tokens)

Pour injecter une valeur (config, instance de tiers, mock), on utilise un token personnalisé :


// src/tasks/tasks.module.ts
import { Module } from '@nestjs/common';
import { TasksService } from './tasks.service';

@Module({
  providers: [
    TasksService,
    {
      // Token symbolique
      provide: 'TASKS_CONFIG',
      // Factory qui construit la valeur
      useFactory: () => ({
        maxTasksPerUser: 100,
        defaultStatus: 'todo',
      }),
    },
  ],
})
export class TasksModule {}

// Utilisation dans le service :
import { Inject, Injectable } from '@nestjs/common';

@Injectable()
export class TasksService {
  constructor(
    @Inject('TASKS_CONFIG')
    private readonly config: { maxTasksPerUser: number; defaultStatus: string },
  ) {}
}
🧠 Pour les développeurs Angular : c'est exactement le même mécanisme que @Inject(TOKEN) avec useFactory en Angular. La syntaxe est identique parce que les deux frameworks partagent l'inspiration Angular DI.

5. Créer une API CRUD complète

Construisons une ressource Task avec les cinq endpoints classiques. Le CLI Nest génère le scaffold en une commande :


# Génère module + controller + service + DTO + entity + tests
nest g resource tasks --no-spec=false
# → choisir REST API
# → générer les CRUD endpoints : Yes

DTO d'entrée


// src/tasks/dto/create-task.dto.ts
import { IsString, IsBoolean, IsOptional, MinLength, MaxLength } from 'class-validator';

export class CreateTaskDto {
  // Validé automatiquement par ValidationPipe (voir section 7)
  @IsString()
  @MinLength(3, { message: 'Le titre doit faire au moins 3 caractères' })
  @MaxLength(120)
  title: string;

  @IsBoolean()
  @IsOptional()
  done?: boolean;
}

// src/tasks/dto/update-task.dto.ts
import { PartialType } from '@nestjs/mapped-types';
import { CreateTaskDto } from './create-task.dto';

// PartialType rend tous les champs optionnels (PATCH)
export class UpdateTaskDto extends PartialType(CreateTaskDto) {}

Controller complet


// src/tasks/tasks.controller.ts
import {
  Controller, Get, Post, Patch, Delete,
  Body, Param, HttpCode, ParseUUIDPipe,
} from '@nestjs/common';
import { TasksService } from './tasks.service';
import { CreateTaskDto } from './dto/create-task.dto';
import { UpdateTaskDto } from './dto/update-task.dto';

@Controller('tasks')
export class TasksController {
  constructor(private readonly tasksService: TasksService) {}

  @Get()
  findAll() {
    return this.tasksService.findAll();
  }

  @Get(':id')
  // ParseUUIDPipe valide le format UUID avant d'atteindre le service
  findOne(@Param('id', new ParseUUIDPipe()) id: string) {
    return this.tasksService.findOne(id);
  }

  @Post()
  @HttpCode(201) // Statut HTTP explicite (par défaut 201 sur POST)
  create(@Body() dto: CreateTaskDto) {
    return this.tasksService.create(dto);
  }

  @Patch(':id')
  update(
    @Param('id', new ParseUUIDPipe()) id: string,
    @Body() dto: UpdateTaskDto,
  ) {
    return this.tasksService.update(id, dto);
  }

  @Delete(':id')
  @HttpCode(204) // 204 No Content après suppression réussie
  remove(@Param('id', new ParseUUIDPipe()) id: string) {
    return this.tasksService.remove(id);
  }
}

Endpoints exposés

Méthode Route Description Status code
GET/tasksListe toutes les tâches200
GET/tasks/:idRécupère une tâche par UUID200 / 404
POST/tasksCrée une tâche201
PATCH/tasks/:idMet à jour une tâche200 / 404
DELETE/tasks/:idSupprime une tâche204 / 404

6. Intégrer TypeORM et PostgreSQL

Le stockage en mémoire n'est utile que pour les tests rapides. Branchons une vraie base PostgreSQL via TypeORM, l'ORM le plus utilisé dans l'écosystème NestJS.

Installation des dépendances


npm install @nestjs/typeorm typeorm pg
npm install --save-dev @types/pg

Configuration via .env


# .env
DB_HOST=localhost
DB_PORT=5432
DB_USER=nest_user
DB_PASSWORD=changeme
DB_NAME=tasks_db

// src/app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TasksModule } from './tasks/tasks.module';

@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true }),
    // Configuration TypeORM asynchrone : injecte ConfigService
    TypeOrmModule.forRootAsync({
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        type: 'postgres',
        host: config.get('DB_HOST'),
        port: config.get('DB_PORT'),
        username: config.get('DB_USER'),
        password: config.get('DB_PASSWORD'),
        database: config.get('DB_NAME'),
        autoLoadEntities: true,
        // synchronize: true en dev seulement, jamais en prod
        synchronize: process.env.NODE_ENV !== 'production',
      }),
    }),
    TasksModule,
  ],
})
export class AppModule {}

Entité Task


// src/tasks/entities/task.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';

@Entity('tasks')
export class Task {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column({ length: 120 })
  title: string;

  @Column({ default: false })
  done: boolean;

  @CreateDateColumn({ name: 'created_at' })
  createdAt: Date;

  @UpdateDateColumn({ name: 'updated_at' })
  updatedAt: Date;
}

Repository injecté dans le service


// src/tasks/tasks.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Task } from './entities/task.entity';
import { TasksController } from './tasks.controller';
import { TasksService } from './tasks.service';

@Module({
  // Enregistre le repository Task dans ce module
  imports: [TypeOrmModule.forFeature([Task])],
  controllers: [TasksController],
  providers: [TasksService],
})
export class TasksModule {}

// src/tasks/tasks.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Task } from './entities/task.entity';
import { CreateTaskDto } from './dto/create-task.dto';
import { UpdateTaskDto } from './dto/update-task.dto';

@Injectable()
export class TasksService {
  constructor(
    // Injection du repository TypeORM lié à l'entité Task
    @InjectRepository(Task)
    private readonly repo: Repository,
  ) {}

  findAll() {
    return this.repo.find({ order: { createdAt: 'DESC' } });
  }

  async findOne(id: string) {
    const task = await this.repo.findOneBy({ id });
    if (!task) throw new NotFoundException(`Task ${id} introuvable`);
    return task;
  }

  create(dto: CreateTaskDto) {
    // create() instancie sans persister, save() écrit en BDD
    const task = this.repo.create(dto);
    return this.repo.save(task);
  }

  async update(id: string, dto: UpdateTaskDto) {
    const task = await this.findOne(id); // 404 si introuvable
    Object.assign(task, dto);
    return this.repo.save(task);
  }

  async remove(id: string) {
    const result = await this.repo.delete(id);
    if (result.affected === 0) {
      throw new NotFoundException(`Task ${id} introuvable`);
    }
  }
}
⚠️ Migrations en production : synchronize: true est dangereux en prod (il modifie le schéma à chaque démarrage). Utilisez typeorm migration:generate et migration:run pour gérer les évolutions de schéma de manière contrôlée.

7. Validation avec class-validator et Pipes

NestJS valide automatiquement les DTO d'entrée si vous activez le ValidationPipe global. Toute requête avec un payload invalide retourne un 400 sans atteindre le controller.

Activer la validation globale


npm install class-validator class-transformer

// src/main.ts
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,             // Retire les champs non déclarés dans le DTO
      forbidNonWhitelisted: true,  // 400 si champ inconnu envoyé
      transform: true,             // Convertit primitives ('5' → 5, 'true' → true)
      transformOptions: {
        enableImplicitConversion: true,
      },
    }),
  );

  // Préfixe global /api/v1
  app.setGlobalPrefix('api/v1');

  await app.listen(3000);
}
bootstrap();

Validation enrichie sur un DTO


// src/tasks/dto/create-task.dto.ts
import {
  IsString, IsBoolean, IsOptional,
  IsEnum, MinLength, MaxLength, IsInt, Min, Max,
} from 'class-validator';

export enum TaskPriority {
  LOW = 'low',
  MEDIUM = 'medium',
  HIGH = 'high',
}

export class CreateTaskDto {
  @IsString()
  @MinLength(3)
  @MaxLength(120)
  title: string;

  @IsOptional()
  @IsString()
  @MaxLength(1000)
  description?: string;

  @IsOptional()
  @IsEnum(TaskPriority, { message: 'priority doit être low, medium ou high' })
  priority?: TaskPriority;

  @IsOptional()
  @IsInt()
  @Min(1)
  @Max(100)
  estimatedHours?: number;

  @IsOptional()
  @IsBoolean()
  done?: boolean;
}

Exemple de réponse d'erreur


{
  "statusCode": 400,
  "message": [
    "title must be longer than or equal to 3 characters",
    "priority doit être low, medium ou high"
  ],
  "error": "Bad Request"
}

8. Guards, Interceptors et Middlewares

NestJS découpe le pipeline de requête en couches spécialisées. Chacune a un rôle précis : ne pas mélanger.

CoucheRôleCas d'usage
Middleware Logique avant le routing Logger HTTP brut, cookies, body parser
Guard Autoriser ou refuser une requête Auth JWT, RBAC, IP whitelist
Interceptor Transformer requête / réponse Logging, cache, sérialisation, mesure de durée
Pipe Transformer / valider un paramètre ValidationPipe, ParseIntPipe, ParseUUIDPipe
Exception Filter Catcher les exceptions Format JSON d'erreur standardisé, Sentry

Guard JWT simple


// src/common/guards/jwt-auth.guard.ts
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
import { Request } from 'express';
import * as jwt from 'jsonwebtoken';

@Injectable()
export class JwtAuthGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const req = context.switchToHttp().getRequest();
    const auth = req.headers.authorization;

    if (!auth || !auth.startsWith('Bearer ')) {
      throw new UnauthorizedException('Token manquant');
    }

    try {
      const token = auth.slice(7);
      const payload = jwt.verify(token, process.env.JWT_SECRET as string);
      // Attache l'utilisateur à la requête pour les handlers suivants
      (req as any).user = payload;
      return true;
    } catch {
      throw new UnauthorizedException('Token invalide ou expiré');
    }
  }
}

// Utilisation sur un controller
// @UseGuards(JwtAuthGuard)
// @Controller('tasks')
// export class TasksController { ... }

Interceptor de logging


// src/common/interceptors/logging.interceptor.ts
import { CallHandler, ExecutionContext, Injectable, Logger, NestInterceptor } from '@nestjs/common';
import { Observable, tap } from 'rxjs';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  private readonly logger = new Logger(LoggingInterceptor.name);

  intercept(context: ExecutionContext, next: CallHandler): Observable {
    const req = context.switchToHttp().getRequest();
    const start = Date.now();

    return next.handle().pipe(
      // tap permet d'observer sans modifier la réponse
      tap(() => {
        const ms = Date.now() - start;
        this.logger.log(`${req.method} ${req.url} - ${ms}ms`);
      }),
    );
  }
}

Filter global pour normaliser les erreurs


// src/common/filters/http-exception.filter.ts
import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus } from '@nestjs/common';
import { Response } from 'express';

@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const res = ctx.getResponse();
    const req = ctx.getRequest();

    const status =
      exception instanceof HttpException
        ? exception.getStatus()
        : HttpStatus.INTERNAL_SERVER_ERROR;

    const message =
      exception instanceof HttpException
        ? exception.getResponse()
        : 'Internal server error';

    // Format de réponse standardisé pour le front Angular
    res.status(status).json({
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: req.url,
      error: message,
    });
  }
}

9. Documentation Swagger automatique

NestJS génère une documentation OpenAPI complète à partir des decorators du code. Plus besoin de maintenir un fichier YAML à part.


npm install @nestjs/swagger swagger-ui-express

// src/main.ts
import { NestFactory } from '@nestjs/core';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  const config = new DocumentBuilder()
    .setTitle('Tasks API')
    .setDescription('API CRUD construite avec NestJS')
    .setVersion('1.0')
    .addBearerAuth()
    .build();

  const document = SwaggerModule.createDocument(app, config);
  // UI accessible sur http://localhost:3000/docs
  SwaggerModule.setup('docs', app, document);

  await app.listen(3000);
}
bootstrap();

Enrichir la doc via decorators


// src/tasks/dto/create-task.dto.ts
import { ApiProperty } from '@nestjs/swagger';
import { IsString, MinLength } from 'class-validator';

export class CreateTaskDto {
  @ApiProperty({
    description: 'Titre court de la tâche',
    example: 'Préparer la release v2',
    minLength: 3,
    maxLength: 120,
  })
  @IsString()
  @MinLength(3)
  title: string;
}

// src/tasks/tasks.controller.ts
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';

@ApiTags('tasks')
@Controller('tasks')
export class TasksController {
  @Post()
  @ApiOperation({ summary: 'Créer une nouvelle tâche' })
  @ApiResponse({ status: 201, description: 'Tâche créée' })
  @ApiResponse({ status: 400, description: 'Payload invalide' })
  create(@Body() dto: CreateTaskDto) {
    return this.tasksService.create(dto);
  }
}
📘 Bénéfices Swagger : client TypeScript généré automatiquement (openapi-generator), tests manuels via l'UI, contrat partagé avec l'équipe front, validation continue de l'API en CI.

10. Tests unitaires et e2e

NestJS embarque Jest et un TestingModule qui simule le container DI réel. Les tests sont rapides, isolés et reflètent la production.

Test unitaire d'un service avec repository mocké


// src/tasks/tasks.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { NotFoundException } from '@nestjs/common';
import { TasksService } from './tasks.service';
import { Task } from './entities/task.entity';

describe('TasksService', () => {
  let service: TasksService;
  let repo: jest.Mocked>;

  beforeEach(async () => {
    // Mock minimal du repository TypeORM
    const repoMock: Partial>> = {
      find: jest.fn(),
      findOneBy: jest.fn(),
      create: jest.fn(),
      save: jest.fn(),
      delete: jest.fn(),
    };

    const module: TestingModule = await Test.createTestingModule({
      providers: [
        TasksService,
        { provide: getRepositoryToken(Task), useValue: repoMock },
      ],
    }).compile();

    service = module.get(TasksService);
    repo = module.get(getRepositoryToken(Task)) as jest.Mocked>;
  });

  it('findOne lève NotFoundException quand la tâche n\'existe pas', async () => {
    repo.findOneBy.mockResolvedValue(null);
    await expect(service.findOne('uuid-inexistant'))
      .rejects.toBeInstanceOf(NotFoundException);
  });

  it('create persiste une nouvelle tâche', async () => {
    const dto = { title: 'Test' };
    const created = { id: 'uuid-1', title: 'Test', done: false } as Task;
    repo.create.mockReturnValue(created);
    repo.save.mockResolvedValue(created);

    const result = await service.create(dto);

    expect(repo.create).toHaveBeenCalledWith(dto);
    expect(repo.save).toHaveBeenCalledWith(created);
    expect(result).toEqual(created);
  });
});

Test end-to-end avec Supertest


// test/tasks.e2e-spec.ts
import { Test } from '@nestjs/testing';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';

describe('Tasks (e2e)', () => {
  let app: INestApplication;

  beforeAll(async () => {
    const moduleFixture = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();

    app = moduleFixture.createNestApplication();
    app.useGlobalPipes(new ValidationPipe({ whitelist: true }));
    await app.init();
  });

  afterAll(async () => {
    await app.close();
  });

  it('POST /api/v1/tasks retourne 400 si title manquant', () => {
    return request(app.getHttpServer())
      .post('/api/v1/tasks')
      .send({})
      .expect(400);
  });

  it('POST /api/v1/tasks crée une tâche valide', () => {
    return request(app.getHttpServer())
      .post('/api/v1/tasks')
      .send({ title: 'Tâche e2e' })
      .expect(201)
      .expect((res) => {
        expect(res.body.title).toBe('Tâche e2e');
        expect(res.body.done).toBe(false);
      });
  });
});

Commandes


npm run test          # Unitaires
npm run test:watch    # Mode watch
npm run test:cov      # Couverture
npm run test:e2e      # Tests end-to-end

11. Déploiement en production

Dockerfile multi-stage


# Stage 1 : build TypeScript
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 2 : runtime minimal
FROM node:20-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=builder /app/dist ./dist
EXPOSE 3000
CMD ["node", "dist/main.js"]

docker-compose avec PostgreSQL


version: '3.9'
services:
  api:
    build: .
    ports:
      - "3000:3000"
    environment:
      DB_HOST: postgres
      DB_PORT: 5432
      DB_USER: nest_user
      DB_PASSWORD: ${DB_PASSWORD}
      DB_NAME: tasks_db
      JWT_SECRET: ${JWT_SECRET}
      NODE_ENV: production
    depends_on:
      - postgres
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: nest_user
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: tasks_db
    volumes:
      - pgdata:/var/lib/postgresql/data
volumes:
  pgdata:

Hardening production


// src/main.ts (production-ready)
import helmet from 'helmet';
import * as compression from 'compression';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // Sécurité HTTP : Content-Security-Policy, HSTS, X-Frame-Options...
  app.use(helmet());
  app.use(compression());

  app.enableCors({
    origin: process.env.CORS_ORIGIN?.split(',') ?? [],
    credentials: true,
  });

  app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
  app.setGlobalPrefix('api/v1');

  // Graceful shutdown : ferme les connexions DB proprement
  app.enableShutdownHooks();

  await app.listen(process.env.PORT || 3000);
}
bootstrap();
🚀 Process manager : en VPS ou bare metal, lancez NestJS via PM2 (pm2 start dist/main.js -i max) pour profiter du clustering Node natif et du restart automatique. Voir l'article dédié PM2.

Conclusion et checklist

NestJS est aujourd'hui le framework Node.js d'entreprise le plus mûr. Sa structure modulaire, son IoC container et son écosystème (TypeORM, Swagger, GraphQL, microservices) en font un choix naturel pour les équipes Angular ou pour toute API destinée à grandir au-delà du prototype.

✅ Checklist mise en production

  • Variables d'environnement chargées via @nestjs/config et validées
  • ValidationPipe global avec whitelist et forbidNonWhitelisted
  • helmet et compression activés
  • CORS restreint aux domaines autorisés
  • JWT signé avec un secret long, refresh token séparé
  • Migrations TypeORM versionnées (jamais synchronize: true en prod)
  • Logger structuré (Pino ou Winston) + Sentry pour les erreurs
  • Tests unitaires et e2e dans la CI
  • Swagger restreint ou désactivé en prod selon le contexte
  • Health check (@nestjs/terminus) exposé pour Kubernetes
  • Graceful shutdown activé (enableShutdownHooks)
  • Image Docker multi-stage, image finale < 200 Mo
🎯 Stack récapitulée :
  • Framework : NestJS 10 + TypeScript 5
  • HTTP adapter : Express (ou Fastify pour +30% throughput)
  • ORM : TypeORM + PostgreSQL 16
  • Validation : class-validator + ValidationPipe global
  • Documentation : @nestjs/swagger
  • Tests : Jest + Supertest
  • Infrastructure : Docker multi-stage + PM2 ou Kubernetes

Partager