À propos de ce cours
Objectifs de la leçon
- Choisir le bon code de statut et le fixer avec @HttpCode.
- Signaler les erreurs avec les exceptions HTTP de NestJS.
- Ajouter des en-têtes fixes ou calculés.
- Rediriger de façon fixe ou dynamique.
Succès et redirections se règlent par décorateurs ; les erreurs se signalent en levant une exception.
Le contrôleur d'exemple
📄 src/groupes.controller.ts
import {
Body, ConflictException, Controller, Delete, ForbiddenException, Get, Header, HttpCode, HttpException,
HttpStatus, NotFoundException, Param, ParseIntPipe, Post, Query, Redirect, Res,
} from '@nestjs/common';
import type { Response } from 'express';
interface Groupe { id: number; code: string; capacite: number; verrouille: boolean }
@Controller('groupes')
export class GroupesController {
private groupes: Groupe[] = [
{ id: 1, code: 'DEV101', capacite: 24, verrouille: false },
{ id: 2, code: 'DEV102', capacite: 20, verrouille: true },
];
@Get()
@Header('Cache-Control', 'public, max-age=60') // en-tête fixe
findAll(@Res({ passthrough: true }) res: Response) {
res.setHeader('X-Total-Count', String(this.groupes.length)); // en-tête calculé
return this.groupes;
}
@Post()
create(@Body() body: { code: string; capacite: number }, @Res({ passthrough: true }) res: Response) {
if (this.groupes.some((g) => g.code === body.code)) {
throw new ConflictException(`Le groupe ${body.code} existe déjà`); // 409
}
const g = { id: this.groupes.length + 1, verrouille: false, ...body };
this.groupes.push(g);
res.location(`/api/groupes/${g.id}`); // 201 + Location
return g;
}
@Post('recalculer')
@HttpCode(HttpStatus.OK) // 200 au lieu de 201
recalculer() {
return { recalcule: true };
}
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT) // 204 : pas de corps
remove(@Param('id', ParseIntPipe) id: number) {
const g = this.groupes.find((x) => x.id === id);
if (!g) throw new NotFoundException(`Groupe ${id} introuvable`); // 404
if (g.verrouille) throw new ForbiddenException('Ce groupe est verrouillé'); // 403
this.groupes = this.groupes.filter((x) => x.id !== id);
}
@Get('quota')
quota() {
throw new HttpException( // statut et corps libres
{ statusCode: 429, erreur: 'Trop de requêtes', reessayerDans: '30 s' },
HttpStatus.TOO_MANY_REQUESTS,
);
}
@Get('emploi-du-temps')
@Redirect('https://apcpedagogie.com', 302) // redirection fixe
emploiDuTemps(@Query('groupe') groupe?: string) {
if (groupe) return { url: `/edt/${groupe.toLowerCase()}`, statusCode: 307 }; // dynamique
}
}
Requêtes envoyées et réponses réellement obtenues :
GET /api/groupes → 200 [{"id":1,"code":"DEV101","capacite":24,"verrouille":false},{"id":2,"code":"DEV102","capacite":20,"verrouille":true}] [cache-control: public, max-age=60] [x-total-count: 2]
POST /api/groupes → 201 {"id":3,"verrouille":false,"code":"INF101","capacite":18} [location: /api/groupes/3]
POST /api/groupes → 409 {"message":"Le groupe DEV101 existe déjà","error":"Conflict","statusCode":409}
POST /api/groupes/recalculer → 200 {"recalcule":true}
DELETE /api/groupes/3 → 204
DELETE /api/groupes/2 → 403 {"message":"Ce groupe est verrouillé","error":"Forbidden","statusCode":403}
DELETE /api/groupes/9 → 404 {"message":"Groupe 9 introuvable","error":"Not Found","statusCode":404}
GET /api/groupes/quota → 429 {"statusCode":429,"erreur":"Trop de requêtes","reessayerDans":"30 s"}
GET /api/groupes/emploi-du-temps → 302 Found. Redirecting to https://apcpedagogie.com [location: https://apcpedagogie.com]
GET /api/groupes/emploi-du-temps?groupe=DEV101 → 307 Temporary Redirect. Redirecting to /edt/dev101 [location: /edt/dev101]
1. Les statuts de succès et @HttpCode
- Par défaut : 200, et 201 pour
@Post(). @HttpCode(HttpStatus.OK)surrecalculer: un POST qui ne crée rien répond 200.@HttpCode(HttpStatus.NO_CONTENT)surremove: 204, et le corps est vide même si la méthode renvoyait quelque chose.- L'énumération
HttpStatusévite les « nombres magiques » :HttpStatus.CREATED,HttpStatus.NO_CONTENT,HttpStatus.TOO_MANY_REQUESTS…
2. Les erreurs : lever une exception
On ne « renvoie » pas une erreur : on lève une exception. NestJS l'intercepte (filtre d'exception intégré, leçon 5.5) et construit la réponse { message, error, statusCode }. L'exception peut être levée dans le contrôleur ou, mieux, dans le service.
| Exception | Statut | Quand l'utiliser |
|---|---|---|
BadRequestException | 400 | données reçues incorrectes |
UnauthorizedException | 401 | non authentifié (pas de jeton, jeton invalide) |
ForbiddenException | 403 | authentifié mais pas autorisé |
NotFoundException | 404 | ressource inexistante |
ConflictException | 409 | conflit avec l'existant (doublon) |
UnprocessableEntityException | 422 | données bien formées mais impossibles à traiter |
InternalServerErrorException | 500 | erreur imprévue (à éviter de lever soi-même) |
Pour un statut sans classe dédiée ou une forme de réponse particulière, new HttpException(corps, statut) accepte un objet libre (voir la route quota: 429 avec un corps personnalisé).
Erreur non HTTP: si le code lève une erreur ordinaire (throw new Error('…'), ou un bug comme undefined.nom), NestJS répond 500 avec « Internal server error », sans révéler le message au client, et affiche l'erreur dans le terminal. C'est voulu : les détails techniques ne doivent pas fuiter. Levez des exceptions HTTP pour les erreurs prévues.
3. Les en-têtes de réponse
- En-tête fixe:
@Header('Cache-Control', 'public, max-age=60'). - En-tête calculé:
@Res({ passthrough: true })puisres.setHeader('X-Total-Count', …)(leçon 6.1). - Location après une création: la bonne pratique REST est de renvoyer, avec le 201, l'adresse de la nouvelle ressource (
res.location('/api/groupes/3')).
4. Les redirections
@Redirect(url, statut): redirection fixe (302 par défaut).- Redirection dynamique: la méthode renvoie
{ url, statusCode }, qui remplace les valeurs du décorateur. Si elle ne renvoie rien, la redirection fixe s'applique. - 301: déplacement définitif (les navigateurs et moteurs de recherche le mémorisent) ; 302 / 307: temporaire (307 conserve la méthode et le corps, utile pour un POST).
Activités
Activité 1 Le bon statut
Quel statut et quel mécanisme NestJS pour : (a) suppression réussie ; (b) création d'un stagiaire dont l'e-mail existe déjà ; (c) POST /api/groupes/1/archiver réussi ; (d) consultation d'un groupe inexistant ; (e) stagiaire qui tente de modifier ses propres notes ; (f) requête sans jeton sur une route protégée ; (g) note de 25 sur 20 ?
Voir le corrigé
(a) 204 avec @HttpCode(HttpStatus.NO_CONTENT) (ou 200 en renvoyant l'élément supprimé) ; (b) 409, ConflictException; (c) 200 avec @HttpCode(HttpStatus.OK); (d) 404, NotFoundException; (e) 403, ForbiddenException; (f) 401, UnauthorizedException; (g) 400, BadRequestException (le chapitre 08 le fera automatiquement).
Activité 2 Créer avec Location
Modifiez POST /api/formations pour renvoyer l'en-tête Location de la formation créée, et GET /api/formations pour renvoyer X-Total-Count. Vérifiez les en-têtes dans votre client d'API.
Voir le corrigé
@Post()
create(@Body() dto: CreerFormationDto, @Res({ passthrough: true }) res: Response) {
const f = this.formationsService.create(dto);
res.location(`/api/formations/${f.id}`);
return f;
}
@Get()
findAll(@Res({ passthrough: true }) res: Response) {
const liste = this.formationsService.findAll();
res.setHeader('X-Total-Count', String(liste.length));
return liste;
}
Les valeurs d'en-tête sont des chaînes : String(liste.length). Un front-end Angular lira ces en-têtes avec observe: 'response', mais, en appel depuis un autre domaine, il faudra les autoriser dans CORS avec l'option exposedHeaders.
Activité 3 Doublon et verrou
Dans FormationsService.create, refusez une formation dont l'intitulé existe déjà (sans tenir compte des majuscules) avec un 409. Dans remove, refusez la suppression d'une formation publiée avec un 403 et le message « Dépubliez la formation avant de la supprimer ».
Voir le corrigé
create(dto: CreerFormationDto) {
const existe = this.formations.some((f) => f.intitule.toLowerCase() === dto.intitule.toLowerCase());
if (existe) throw new ConflictException(`La formation « ${dto.intitule} » existe déjà`);
…
}
remove(id: number) {
const f = this.findOne(id);
if (f.publiee) throw new ForbiddenException('Dépubliez la formation avant de la supprimer');
…
}
Activité 4 Erreur imprévue
Ajoutez une route GET /api/formations/bug qui exécute const f: any = undefined; return f.intitule;. Quel statut et quel corps reçoit le client ? Qu'affiche le terminal ? Pourquoi ce comportement est-il souhaitable ?
Voir le corrigé
Le client reçoit 500 { "statusCode": 500, "message": "Internal server error" }; le terminal affiche ERROR [ExceptionsHandler] TypeError: Cannot read properties of undefined (reading 'intitule') avec la pile d'appels. Le développeur dispose de tous les détails, l'utilisateur (ou un attaquant) d'aucun.
Activité 5 Redirections
Ajoutez GET /api/formations/:id/programme qui redirige (302) vers /programmes/<id>, et vers la page générale /programmes si la formation n'est pas publiée. Pourquoi ne pas utiliser 301 ici ?
Voir le corrigé
@Get(':id/programme')
@Redirect('/programmes', 302)
programme(@Param('id', ParseIntPipe) id: number) {
const f = this.formationsService.findOne(id);
if (f.publiee) return { url: `/programmes/${f.id}` };
}
Une redirection 301 est mémorisée par le navigateur : si la formation est publiée plus tard, les visiteurs continueraient d'être envoyés vers l'ancienne adresse. On réserve 301 aux adresses déplacées définitivement.
À retenir
- 200 par défaut, 201 pour POST ; @HttpCode(HttpStatus.X) pour changer, 204 sans corps.
- Les erreurs se lèvent : BadRequest 400, Unauthorized 401, Forbidden 403, NotFound 404, Conflict 409.
- HttpException(corps, statut) pour un cas particulier ; une erreur imprévue donne 500 sans détail.
- @Header pour un en-tête fixe ; passthrough + res.setHeader / res.location pour un en-tête calculé.
- @Redirect(url, statut) ; renvoyer { url, statusCode } pour une redirection dynamique.