Controllers & Providers

Controllers = couche HTTP · Providers = logique métier

La sĂ©paration entre controllers (routes) et providers (services) est fondamentale dans NestJS. Un controller ne fait que recevoir des requĂȘtes et dĂ©lĂ©guer au service appropriĂ©.

Controllers

Un controller gĂšre les requĂȘtes HTTP entrantes et retourne les rĂ©ponses.

Anatomie d'un controller

import {
  Controller, Get, Post, Put, Delete, Patch,
  Body, Param, Query, Headers, Req, Res,
  HttpCode, HttpStatus, ParseIntPipe,
} from '@nestjs/common';
import { Request, Response } from 'express';

@Controller('products')  // Préfixe de route : /products
export class ProductsController {
  constructor(private readonly productsService: ProductsService) {}

  // GET /products
  @Get()
  findAll() {
    return this.productsService.findAll();
  }

  // GET /products/featured
  @Get('featured')   // Route statique AVANT la route paramétrée
  findFeatured() {
    return this.productsService.findFeatured();
  }

  // GET /products/:id
  @Get(':id')
  findOne(@Param('id', ParseIntPipe) id: number) {
    return this.productsService.findOne(id);
  }

  // POST /products
  @Post()
  @HttpCode(HttpStatus.CREATED)
  create(@Body() createProductDto: CreateProductDto) {
    return this.productsService.create(createProductDto);
  }

  // PUT /products/:id
  @Put(':id')
  update(
    @Param('id', ParseIntPipe) id: number,
    @Body() updateProductDto: UpdateProductDto,
  ) {
    return this.productsService.update(id, updateProductDto);
  }

  // DELETE /products/:id
  @Delete(':id')
  @HttpCode(HttpStatus.NO_CONTENT)
  remove(@Param('id', ParseIntPipe) id: number) {
    return this.productsService.remove(id);
  }
}

Routes avancées

Wildcards et patterns

// Wildcard
@Get('ab*cd')
findAll() { ... }  // Correspond Ă  : abcd, ab_cd, abanythingcd

// Préfixe de version
@Controller({ path: 'users', version: '1' })
class UsersControllerV1 { ... }

// Routes imbriquées avec plusieurs contrÎleurs
@Controller('orders/:orderId/items')
class OrderItemsController {
  @Get()
  findAll(@Param('orderId') orderId: string) { ... }
}

Redirection et streaming

// Redirection
@Get('docs')
@Redirect('https://docs.nestjs.com', 302)
getDocs() {}

// Redirection dynamique
@Get('redirect')
redirect() {
  return { url: 'https://nestjs.com', statusCode: 301 };
}

// Réponse brute avec @Res (déconseillé en général)
@Get('stream')
streamFile(@Res() res: Response) {
  const file = createReadStream('/path/to/file.pdf');
  res.set({ 'Content-Type': 'application/pdf' });
  file.pipe(res);
}

Décorateurs HTTP

Tableau des décorateurs principaux

Décorateur RÎle
@Get()Route GET
@Post()Route POST
@Put() / @Patch()Mise Ă  jour complĂšte / partielle
@Delete()Suppression
@HttpCode(201)Code HTTP de retour personnalisé
@Header('key','val')Ajoute un header à la réponse
@Redirect(url, code)Redirige vers une URL

Codes HTTP avec HttpStatus

import { HttpStatus } from '@nestjs/common';

@Post()
@HttpCode(HttpStatus.CREATED)  // 201

@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)  // 204

// Lancer une exception HTTP
throw new HttpException('Not Found', HttpStatus.NOT_FOUND);
// Ou directement :
throw new NotFoundException('User not found');
throw new BadRequestException('Invalid data');
throw new UnauthorizedException();

Params, Query, Body & Headers

@Controller('search')
export class SearchController {
  // Params de route : /search/users/42
  @Get(':resource/:id')
  findOne(
    @Param('resource') resource: string,
    @Param('id', ParseIntPipe) id: number,
  ) { ... }

  // Query string : /search?q=nestjs&limit=10&page=2
  @Get()
  search(
    @Query('q') query: string,
    @Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit: number,
    @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
  ) { ... }

  // Body JSON
  @Post()
  create(@Body() dto: CreateSearchDto) { ... }

  // Body partiel
  @Patch(':id')
  update(@Param('id') id: string, @Body('name') name: string) { ... }

  // Headers
  @Get('secure')
  secure(@Headers('authorization') auth: string) { ... }

  // Objet request complet (Express)
  @Get('full')
  full(@Req() req: Request) {
    return req.ip;
  }
}
ParseIntPipe : Transforme automatiquement le paramÚtre de route (string) en number et lance une BadRequestException si la conversion échoue.

Providers

Un provider est n'importe quelle classe annotée avec @Injectable(). NestJS peut l'injecter automatiquement dans d'autres classes.

Service classique

import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';

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

  async create(dto: CreateUserDto): Promise<User> {
    const user = this.usersRepository.create(dto);
    return this.usersRepository.save(user);
  }

  async findAll(): Promise<User[]> {
    return this.usersRepository.find();
  }

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

  async remove(id: number): Promise<void> {
    await this.findOne(id); // Vérifie l'existence
    await this.usersRepository.delete(id);
  }
}

Types de providers

// 1. Class provider (le plus courant)
providers: [UsersService]

// 2. Value provider
providers: [{ provide: 'CONFIG', useValue: { apiKey: '...' } }]

// 3. Factory provider
providers: [{
  provide: 'DATABASE',
  useFactory: (config: ConfigService) => new Database(config.get('DB_URL')),
  inject: [ConfigService],
}]

// 4. Alias provider
providers: [{ provide: IUserService, useClass: UsersService }]

Injection de dépendances

// Injection par constructeur (recommandé)
@Injectable()
export class OrdersService {
  constructor(
    private readonly usersService: UsersService,
    private readonly productsService: ProductsService,
    private readonly mailerService: MailerService,
  ) {}
}

// Injection de valeur
@Injectable()
export class AppService {
  constructor(
    @Inject('CONFIG') private config: AppConfig,
  ) {}
}

// Injection optionnelle
@Injectable()
export class LoggerService {
  constructor(
    @Optional() @Inject('LOGGER_OPTIONS') private options?: LoggerOptions,
  ) {}
}

Scope des providers

import { Injectable, Scope } from '@nestjs/common';

// DEFAULT : singleton (une seule instance pour toute l'app)
@Injectable()
export class AppService { ... }

// REQUEST : nouvelle instance par requĂȘte
@Injectable({ scope: Scope.REQUEST })
export class RequestScopedService {
  constructor(@Inject(REQUEST) private request: Request) {}
}

// TRANSIENT : nouvelle instance Ă  chaque injection
@Injectable({ scope: Scope.TRANSIENT })
export class TransientService { ... }
Scope par dĂ©faut = singleton. Utilisez REQUEST seulement si vous avez besoin d'accĂ©der aux donnĂ©es de la requĂȘte. Le scope REQUEST a un impact sur les performances car il crĂ©e une nouvelle instance Ă  chaque requĂȘte.
✏ Exercices du module ▶ Mini-projet Module 03 →