Aller au contenu

NestJS

Framework Node.js progressif TypeScript-first pour construire des applications côté serveur évolutives.

CommandeDescription
npm i -g @nestjs/cliInstaller le CLI NestJS globalement
nest new project-nameCréer un nouveau projet NestJS
nest new project-name --strictCréer un projet avec TypeScript strict
nest new project-name -p pnpmCréer un projet avec pnpm
nest new project-name -p yarnCréer un projet avec Yarn
CommandeDescription
npm run start:devDémarrer le serveur de développement avec hot reload
npm run start:debugDémarrer en mode debug avec hot reload
npm run start:prodDémarrer le serveur de production
npm run buildCompiler le projet
npm run testExécuter les tests unitaires
npm run test:watchExécuter les tests en mode watch
npm run test:covExécuter les tests avec rapport de couverture
npm run test:e2eExécuter les tests end-to-end
npm run lintAnalyser le code source
src/
├── app.controller.ts       # Contrôleur racine
├── app.controller.spec.ts  # Test unitaire du contrôleur
├── app.module.ts           # Module racine
├── app.service.ts          # Service racine
├── main.ts                 # Point d'entrée de l'application
├── users/
│   ├── users.module.ts     # Module de fonctionnalité
│   ├── users.controller.ts # Contrôleur de fonctionnalité
│   ├── users.service.ts    # Service de fonctionnalité
│   ├── dto/
│   │   ├── create-user.dto.ts
│   │   └── update-user.dto.ts
│   ├── entities/
│   │   └── user.entity.ts
│   └── users.controller.spec.ts
├── auth/
│   ├── auth.module.ts
│   ├── auth.guard.ts
│   ├── auth.service.ts
│   └── strategies/
│       └── jwt.strategy.ts
└── common/
    ├── filters/
    │   └── http-exception.filter.ts
    ├── interceptors/
    │   └── logging.interceptor.ts
    └── pipes/
        └── validation.pipe.ts
CommandeDescription
nest generate module usersGénérer un nouveau module
nest generate controller usersGénérer un nouveau contrôleur
nest generate service usersGénérer un nouveau service
nest generate resource usersGénérer une ressource CRUD complète (module + contrôleur + service + DTOs)
nest generate guard authGénérer un guard d’authentification
nest generate pipe validationGénérer un pipe de validation
nest generate interceptor loggingGénérer un intercepteur
nest generate middleware loggerGénérer un middleware
nest generate filter http-exceptionGénérer un filtre d’exception
nest generate decorator rolesGénérer un décorateur personnalisé
nest generate class user.entityGénérer une classe simple
nest generate interface userGénérer une interface
nest g res users --no-specGénérer une ressource sans fichiers de test
nest g mo users --flatGénérer sans créer de sous-dossier
nest infoAfficher les informations du projet et les dépendances
# Ressource CRUD complète (workflow le plus courant)
nest g resource users

# Le CLI demande :
# ? What transport layer do you use? REST API
# ? Would you like to generate CRUD entry points? Yes

# Crée :
# src/users/users.module.ts
# src/users/users.controller.ts
# src/users/users.service.ts
# src/users/dto/create-user.dto.ts
# src/users/dto/update-user.dto.ts
# src/users/entities/user.entity.ts
# src/users/users.controller.spec.ts
# Met à jour : src/app.module.ts (importe UsersModule)
CommandeDescription
@Module({ imports: [UsersModule] })Importer un autre module
@Module({ controllers: [UsersController] })Enregistrer les contrôleurs
@Module({ providers: [UsersService] })Enregistrer les fournisseurs/services
@Module({ exports: [UsersService] })Exporter les fournisseurs pour d’autres modules
@Global()Rendre le module disponible globalement (utiliser avec parcimonie)
import { Module, DynamicModule } from '@nestjs/common'

@Module({})
export class DatabaseModule {
  static forRoot(options: DatabaseOptions): DynamicModule {
    return {
      module: DatabaseModule,
      global: true,
      providers: [
        { provide: 'DATABASE_OPTIONS', useValue: options },
        DatabaseService,
      ],
      exports: [DatabaseService],
    }
  }

  static forRootAsync(options: DatabaseAsyncOptions): DynamicModule {
    return {
      module: DatabaseModule,
      global: true,
      imports: options.imports || [],
      providers: [
        {
          provide: 'DATABASE_OPTIONS',
          useFactory: options.useFactory,
          inject: options.inject || [],
        },
        DatabaseService,
      ],
      exports: [DatabaseService],
    }
  }
}

// Utilisation dans AppModule
@Module({
  imports: [
    DatabaseModule.forRootAsync({
      imports: [ConfigModule],
      useFactory: (config: ConfigService) => ({
        host: config.get('DB_HOST'),
        port: config.get('DB_PORT'),
      }),
      inject: [ConfigService],
    }),
  ],
})
export class AppModule {}
CommandeDescription
@Controller('users')Définir un contrôleur avec préfixe de route
@Get()Gérer les requêtes GET
@Post()Gérer les requêtes POST
@Put(':id')Gérer les requêtes PUT avec paramètre
@Patch(':id')Gérer les requêtes PATCH
@Delete(':id')Gérer les requêtes DELETE
@All('*')Gérer toutes les méthodes HTTP
CommandeDescription
@Param('id') id: stringExtraire un paramètre de route
@Param('id', ParseIntPipe) id: numberExtraire et convertir en entier
@Query('page') page: stringExtraire un paramètre de requête
@Query() query: PaginationDtoExtraire tous les paramètres de requête en DTO
@Body() dto: CreateUserDtoExtraire et typer le corps de la requête
@Body('email') email: stringExtraire un champ spécifique du corps
@Headers('authorization') auth: stringExtraire un en-tête de requête
@Ip() ip: stringExtraire l’adresse IP du client
@Req() request: RequestAccéder à l’objet request brut
@Res() response: ResponseAccéder à l’objet response brut
@HttpCode(201)Définir un code de statut HTTP personnalisé
@Header('Cache-Control', 'none')Définir un en-tête de réponse
@Redirect('/new-url', 301)Réponse de redirection
import {
  Controller, Get, Post, Put, Delete, Param, Query, Body,
  HttpCode, HttpStatus, ParseIntPipe, UseGuards, UseInterceptors,
} from '@nestjs/common'
import { UsersService } from './users.service'
import { CreateUserDto } from './dto/create-user.dto'
import { UpdateUserDto } from './dto/update-user.dto'
import { JwtAuthGuard } from '../auth/jwt-auth.guard'
import { LoggingInterceptor } from '../common/interceptors/logging.interceptor'

@Controller('users')
@UseInterceptors(LoggingInterceptor)
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Post()
  @HttpCode(HttpStatus.CREATED)
  create(@Body() createUserDto: CreateUserDto) {
    return this.usersService.create(createUserDto)
  }

  @Get()
  findAll(
    @Query('page', new ParseIntPipe({ optional: true })) page = 1,
    @Query('limit', new ParseIntPipe({ optional: true })) limit = 10,
  ) {
    return this.usersService.findAll(page, limit)
  }

  @Get(':id')
  findOne(@Param('id', ParseIntPipe) id: number) {
    return this.usersService.findOne(id)
  }

  @Put(':id')
  @UseGuards(JwtAuthGuard)
  update(
    @Param('id', ParseIntPipe) id: number,
    @Body() updateUserDto: UpdateUserDto,
  ) {
    return this.usersService.update(id, updateUserDto)
  }

  @Delete(':id')
  @UseGuards(JwtAuthGuard)
  @HttpCode(HttpStatus.NO_CONTENT)
  remove(@Param('id', ParseIntPipe) id: number) {
    return this.usersService.remove(id)
  }
}
CommandeDescription
@Injectable()Marquer une classe comme fournisseur injectable
constructor(private usersService: UsersService)Injecter un service via le constructeur
@Inject('TOKEN') private configInjecter par jeton personnalisé
{ provide: 'TOKEN', useValue: value }Enregistrer un fournisseur de valeur
{ provide: 'TOKEN', useFactory: () => ... }Enregistrer un fournisseur de fabrique
{ provide: 'TOKEN', useClass: MyClass }Enregistrer un fournisseur de classe
{ provide: 'TOKEN', useExisting: OtherService }Alias d’un fournisseur existant
@Optional() private service?: MyServiceInjection de dépendance optionnelle
{ provide: 'TOKEN', scope: Scope.REQUEST }Fournisseur à portée de requête
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common'
import { InjectRepository } from '@nestjs/typeorm'
import { Repository } from 'typeorm'
import { User } from './entities/user.entity'
import { CreateUserDto } from './dto/create-user.dto'
import { UpdateUserDto } from './dto/update-user.dto'

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private readonly usersRepo: Repository<User>,
  ) {}

  async create(dto: CreateUserDto): Promise<User> {
    const exists = await this.usersRepo.findOneBy({ email: dto.email })
    if (exists) {
      throw new ConflictException('Email already registered')
    }
    const user = this.usersRepo.create(dto)
    return this.usersRepo.save(user)
  }

  async findAll(page: number, limit: number) {
    const [items, total] = await this.usersRepo.findAndCount({
      skip: (page - 1) * limit,
      take: limit,
      order: { createdAt: 'DESC' },
    })
    return { items, total, page, limit }
  }

  async findOne(id: number): Promise<User> {
    const user = await this.usersRepo.findOneBy({ id })
    if (!user) throw new NotFoundException(`User #${id} not found`)
    return user
  }

  async update(id: number, dto: UpdateUserDto): Promise<User> {
    const user = await this.findOne(id)
    Object.assign(user, dto)
    return this.usersRepo.save(user)
  }

  async remove(id: number): Promise<void> {
    const result = await this.usersRepo.delete(id)
    if (result.affected === 0) {
      throw new NotFoundException(`User #${id} not found`)
    }
  }
}
npm install class-validator class-transformer
// main.ts - activer la validation globale
import { ValidationPipe } from '@nestjs/common'

async function bootstrap() {
  const app = await NestFactory.create(AppModule)
  app.useGlobalPipes(new ValidationPipe({
    whitelist: true,           // supprimer les propriétés inconnues
    forbidNonWhitelisted: true, // rejeter les propriétés inconnues
    transform: true,           // auto-transformer les payloads en types DTO
    transformOptions: {
      enableImplicitConversion: true, // convertir les chaînes de requête en nombres/booléens
    },
  }))
  await app.listen(3000)
}
import {
  IsString, IsEmail, IsOptional, IsInt, Min, Max,
  MinLength, MaxLength, IsEnum, ValidateNested, IsArray,
} from 'class-validator'
import { Type } from 'class-transformer'
import { PartialType, OmitType, PickType, IntersectionType } from '@nestjs/mapped-types'

export class CreateUserDto {
  @IsString()
  @MinLength(2)
  @MaxLength(50)
  name: string

  @IsEmail()
  email: string

  @IsString()
  @MinLength(8)
  password: string

  @IsOptional()
  @IsEnum(Role)
  role?: Role

  @IsOptional()
  @ValidateNested()
  @Type(() => AddressDto)
  address?: AddressDto
}

// Générer automatiquement le DTO de mise à jour (tous les champs optionnels)
export class UpdateUserDto extends PartialType(CreateUserDto) {}

// Sélectionner des champs spécifiques
export class LoginDto extends PickType(CreateUserDto, ['email', 'password']) {}

// Omettre des champs spécifiques
export class PublicUserDto extends OmitType(CreateUserDto, ['password']) {}
CommandeDescription
@UseGuards(AuthGuard)Appliquer un guard au contrôleur ou à la route
@UseGuards(AuthGuard, RolesGuard)Chaîner plusieurs guards
app.useGlobalGuards(new AuthGuard())Enregistrer un guard global
canActivate(context: ExecutionContext)Implémenter la logique du guard
@SetMetadata('roles', ['admin'])Définir des métadonnées personnalisées pour les guards
ReflectorLire les métadonnées dans les guards
npm install @nestjs/jwt @nestjs/passport passport passport-jwt
npm install -D @types/passport-jwt
// auth/auth.module.ts
@Module({
  imports: [
    UsersModule,
    JwtModule.registerAsync({
      imports: [ConfigModule],
      useFactory: (config: ConfigService) => ({
        secret: config.get('JWT_SECRET'),
        signOptions: { expiresIn: '1h' },
      }),
      inject: [ConfigService],
    }),
  ],
  providers: [AuthService, JwtStrategy],
  controllers: [AuthController],
  exports: [AuthService],
})
export class AuthModule {}

// auth/jwt.strategy.ts
import { Injectable } from '@nestjs/common'
import { PassportStrategy } from '@nestjs/passport'
import { ExtractJwt, Strategy } from 'passport-jwt'

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(config: ConfigService) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: config.get('JWT_SECRET'),
    })
  }

  validate(payload: { sub: number; email: string }) {
    return { id: payload.sub, email: payload.email }
  }
}

// auth/jwt-auth.guard.ts
import { Injectable } from '@nestjs/common'
import { AuthGuard } from '@nestjs/passport'

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}

// Utilisation
@Controller('profile')
@UseGuards(JwtAuthGuard)
export class ProfileController {
  @Get()
  getProfile(@Req() req) {
    return req.user // injecté par JwtStrategy.validate()
  }
}
// roles.decorator.ts
import { SetMetadata } from '@nestjs/common'
export const Roles = (...roles: string[]) => SetMetadata('roles', roles)

// roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'
import { Reflector } from '@nestjs/core'

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.getAllAndOverride<string[]>('roles', [
      context.getHandler(),
      context.getClass(),
    ])
    if (!requiredRoles) return true
    const { user } = context.switchToHttp().getRequest()
    return requiredRoles.some(role => user.roles?.includes(role))
  }
}

// Utilisation
@Controller('admin')
@UseGuards(JwtAuthGuard, RolesGuard)
export class AdminController {
  @Get('dashboard')
  @Roles('admin')
  getDashboard() {
    return { message: 'Admin dashboard' }
  }

  @Delete('users/:id')
  @Roles('admin', 'superadmin')
  removeUser(@Param('id', ParseIntPipe) id: number) {
    return this.usersService.remove(id)
  }
}
import { Injectable, NestMiddleware } from '@nestjs/common'
import { Request, Response, NextFunction } from 'express'

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    const start = Date.now()
    res.on('finish', () => {
      const duration = Date.now() - start
      console.log(`${req.method} ${req.path} ${res.statusCode} ${duration}ms`)
    })
    next()
  }
}

// Enregistrer dans le module
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .forRoutes('*')
  }
}
import {
  Injectable, NestInterceptor, ExecutionContext, CallHandler,
} from '@nestjs/common'
import { Observable, map, tap } from 'rxjs'

// Transformer la forme de la réponse
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, { data: T }> {
  intercept(context: ExecutionContext, next: CallHandler): Observable<{ data: T }> {
    return next.handle().pipe(
      map(data => ({ data, timestamp: new Date().toISOString() })),
    )
  }
}

// Mesurer le temps d'exécution
@Injectable()
export class TimingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const start = Date.now()
    return next.handle().pipe(
      tap(() => {
        const duration = Date.now() - start
        const response = context.switchToHttp().getResponse()
        response.setHeader('X-Response-Time', `${duration}ms`)
      }),
    )
  }
}
CommandeDescription
npm install @nestjs/typeorm typeorm pgInstaller TypeORM avec PostgreSQL
TypeOrmModule.forRoot({ type: 'postgres', ... })Configurer la connexion dans AppModule
TypeOrmModule.forFeature([User])Enregistrer l’entité dans le module de fonctionnalité
@InjectRepository(User) private repo: Repository<User>Injecter le dépôt
npm install @prisma/client
npm install -D prisma
npx prisma init
// prisma/prisma.service.ts
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common'
import { PrismaClient } from '@prisma/client'

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
  async onModuleInit() {
    await this.$connect()
  }

  async onModuleDestroy() {
    await this.$disconnect()
  }
}

// prisma/prisma.module.ts
@Global()
@Module({
  providers: [PrismaService],
  exports: [PrismaService],
})
export class PrismaModule {}

// Utilisation dans un service
@Injectable()
export class UsersService {
  constructor(private prisma: PrismaService) {}

  findAll() {
    return this.prisma.user.findMany()
  }

  findOne(id: number) {
    return this.prisma.user.findUniqueOrThrow({ where: { id } })
  }
}
CommandeDescription
throw new BadRequestException('Invalid input')400 Bad Request
throw new UnauthorizedException('Login required')401 Unauthorized
throw new ForbiddenException('Access denied')403 Forbidden
throw new NotFoundException('User not found')404 Not Found
throw new ConflictException('Email taken')409 Conflict
throw new UnprocessableEntityException('Validation failed')422 Unprocessable
throw new InternalServerErrorException()500 Internal Server Error
import {
  ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus,
} from '@nestjs/common'
import { Request, Response } from 'express'

@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp()
    const response = ctx.getResponse<Response>()
    const request = ctx.getRequest<Request>()

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

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

    response.status(status).json({
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
      message: typeof message === 'string' ? message : (message as any).message,
    })
  }
}

// Enregistrer globalement dans main.ts
app.useGlobalFilters(new AllExceptionsFilter())
npm install @nestjs/config
// app.module.ts
import { ConfigModule, ConfigService } from '@nestjs/config'

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      envFilePath: ['.env.local', '.env'],
      validationSchema: Joi.object({
        NODE_ENV: Joi.string().valid('development', 'production', 'test').default('development'),
        PORT: Joi.number().default(3000),
        DATABASE_URL: Joi.string().required(),
        JWT_SECRET: Joi.string().required(),
      }),
    }),
  ],
})
export class AppModule {}

// Utilisation dans n'importe quel service
@Injectable()
export class AppService {
  constructor(private config: ConfigService) {}

  getPort(): number {
    return this.config.get<number>('PORT', 3000)
  }
}
import { Test, TestingModule } from '@nestjs/testing'
import { UsersService } from './users.service'
import { getRepositoryToken } from '@nestjs/typeorm'
import { User } from './entities/user.entity'

describe('UsersService', () => {
  let service: UsersService

  const mockRepository = {
    create: jest.fn(),
    save: jest.fn(),
    findOneBy: jest.fn(),
    findAndCount: jest.fn(),
    delete: jest.fn(),
  }

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        UsersService,
        { provide: getRepositoryToken(User), useValue: mockRepository },
      ],
    }).compile()

    service = module.get<UsersService>(UsersService)
  })

  it('should create a user', async () => {
    const dto = { name: 'Alice', email: 'alice@example.com', password: 'secret123' }
    mockRepository.findOneBy.mockResolvedValue(null)
    mockRepository.create.mockReturnValue(dto)
    mockRepository.save.mockResolvedValue({ id: 1, ...dto })

    const result = await service.create(dto)
    expect(result.id).toBe(1)
    expect(mockRepository.save).toHaveBeenCalled()
  })
})
import { Test, TestingModule } from '@nestjs/testing'
import { INestApplication, ValidationPipe } from '@nestjs/common'
import * as request from 'supertest'
import { AppModule } from '../src/app.module'

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

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

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

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

  it('/users (POST) creates a user', () => {
    return request(app.getHttpServer())
      .post('/users')
      .send({ name: 'Alice', email: 'alice@example.com', password: 'secret123' })
      .expect(201)
      .expect((res) => {
        expect(res.body.name).toBe('Alice')
        expect(res.body.id).toBeDefined()
      })
  })

  it('/users (GET) returns paginated list', () => {
    return request(app.getHttpServer())
      .get('/users?page=1&limit=10')
      .expect(200)
      .expect((res) => {
        expect(res.body.items).toBeInstanceOf(Array)
        expect(res.body.total).toBeDefined()
      })
  })
})
  1. Utiliser le générateur de ressourcesnest g resource crée l’ensemble de l’échafaudage CRUD (module, contrôleur, service, DTOs, entité, test) en quelques secondes et maintient la cohérence de votre projet.

  2. Activer le ValidationPipe global avec whitelist — Définissez whitelist: true et forbidNonWhitelisted: true pour automatiquement supprimer ou rejeter les champs inattendus de toutes les requêtes.

  3. Utiliser les types mappés pour les DTOsPartialType, PickType, OmitType et IntersectionType gardent vos DTOs DRY en dérivant les DTOs de mise à jour/requête depuis le DTO de création.

  4. Préférer l’injection par constructeur — Laissez NestJS gérer l’injection de dépendances via les constructeurs plutôt que d’utiliser des jetons @Inject() manuels sauf si vous avez besoin de fournisseurs personnalisés.

  5. Garder les contrôleurs légers — Les contrôleurs devraient seulement parser l’entrée et retourner la sortie. Déplacez toute la logique métier dans les services pour la testabilité et la réutilisation.

  6. Utiliser ConfigModule pour tout accès à l’environnement — Ne lisez jamais process.env directement dans les services. Utilisez ConfigService avec des schémas de validation pour que les variables manquantes échouent rapidement au démarrage.

  7. Limiter la portée des guards et intercepteurs de manière appropriée — Appliquez-les au niveau du contrôleur ou de la route avec des décorateurs plutôt que globalement, sauf s’ils s’appliquent vraiment partout.

  8. Écrire des tests unitaires et E2E — Testez unitairement les services avec des dépendances mockées en utilisant Test.createTestingModule(), et testez les endpoints HTTP en E2E avec supertest.

  9. Utiliser Prisma ou TypeORM de manière cohérente — Choisissez un ORM et utilisez-le dans tout le projet. Prisma offre une meilleure sécurité des types ; TypeORM offre plus de flexibilité avec les décorateurs.

  10. Structurer par fonctionnalité, pas par type — Groupez les fichiers par domaine (users/, auth/, orders/) plutôt que par type (controllers/, services/). Cela garde le code lié ensemble et les modules autonomes.

  11. Gérer les erreurs avec des exceptions — Lancez les exceptions HTTP intégrées (NotFoundException, ConflictException) depuis les services. Utilisez les filtres d’exception pour un formatage d’erreur personnalisé.

  12. Utiliser async/await de manière cohérente — NestJS gère les promesses nativement. Retournez des valeurs asynchrones depuis les méthodes de service et laissez le framework sérialiser la réponse.