Avez-vous une question? (216) 97 656 803 hajjriadh@gmail.com
NestJS

TP 4 : Réaliser une API TodoList (CRUD en mémoire) avec NestJS

Ce TP fait réaliser une API de gestion de tâches (TodoList) complète avec NestJS, publiée sur GitHub dès le départ. Les données sont en mémoire, mais l'architecture est celle d'une vraie application :...

0 chapitres 0 QCM Accès gratuit
TP 4 : Réaliser une API TodoList (CRUD en mémoire) avec NestJS

À propos de ce cours

Ce TP fait réaliser une API de gestion de tâches (TodoList) complète avec NestJS, publiée sur GitHub dès le départ. Les données sont en mémoire, mais l'architecture est celle d'une vraie application : un service métier singleton avec ses règles, un générateur d'identifiants interchangeable (useClass, séquentiel ou UUID), une horloge injectée par jeton Symbol, une configuration produite par useFactory à partir de variables d'environnement et un contexte utilisateur de portée requête. Le CRUD comprend filtres, recherche, tri, terminaison, suppression groupée et statistiques. Correction complète vérifiée et grille d'évaluation.

Objectifs du TP

  • Concevoir une API CRUD complète avec une séparation stricte contrôleur / service.
  • Injecter des dépendances par jeton (Symbol, classe abstraite) avec useClass, useValue et useFactory.
  • Changer d'implémentation par configuration, sans modifier le service.
  • Utiliser un provider de portée requête à bon escient.
  • Publier le projet sur GitHub dès le départ, un commit par partie.

Durée: 4 h  |  Prérequis: chapitres 05 à 07  |  Livrable: le dépôt api-todolist avec l'étiquette v1.0.0.

Architecture du TP 4 TodoList

Un service singleton, trois providers personnalisés et un contexte de portée requête.

Cahier des charges

L'équipe pédagogique veut une API de gestion de tâches pour organiser ses préparations (cours, corrections, réunions). Une tâche a la forme suivante :

export const STATUTS = ['a-faire', 'en-cours', 'terminee'] as const;
export type Statut = (typeof STATUTS)[number];

export const PRIORITES = ['basse', 'normale', 'haute'] as const;
export type Priorite = (typeof PRIORITES)[number];

export interface Tache {
  id: string;
  titre: string;
  description?: string;
  statut: Statut;
  priorite: Priorite;
  echeance?: string;          // AAAA-MM-JJ
  creePar: string;
  creeLe: string;
  modifieLe: string;
}

RouteRôleRéponses
GET /api/v1/taches?statut=&priorite=&q=lister, filtrer, rechercher ; tri par échéance (sans échéance en dernier) ; en-tête X-Total-Count200 ; 400 si statut ou priorité inconnus
GET /api/v1/taches/statistiquestotal, nombre par statut, nombre en retard, pourcentage d'avancement200
GET /api/v1/taches/:idune tâche200 ; 404
POST /api/v1/tachescréer : titre obligatoire, priorité par défaut issue de la configuration, échéance au format AAAA-MM-JJ, auteur lu dans X-Utilisateur (« anonyme » sinon)201 + Location; 400 ; 409 si le nombre maximal est atteint
PATCH /api/v1/taches/:idmodifier titre, description, statut, priorité, échéance (les autres champs sont ignorés)200 ; 400 ; 404
PATCH /api/v1/taches/:id/terminerpasser au statut « terminee »200 ; 409 si déjà terminée
DELETE /api/v1/taches/termineessupprimer toutes les tâches terminées200 { supprimees: n }
DELETE /api/v1/taches/:idsupprimer une tâche204 ; 404
GET /santeétat du service200
Partie 1 Créer le projet et le dépôt GitHub
  1. nest new api-todolist --package-manager npm --strict, puis créez le dépôt GitHub public api-todolist et poussez (« chore: création du projet »).
  2. Générez nest g mo taches, nest g s taches --no-spec, nest g co taches --no-spec; supprimez AppController et AppService.
  3. Dans main.ts: préfixe api (sauf sante), versionnement URI (defaultVersion: '1'), CORS avec exposedHeaders: ['Location', 'X-Total-Count'].
  4. Commit : chore: structure du projet.
Partie 2 Le modèle et les providers communs
  1. src/taches/tache.model.ts: listes STATUTS et PRIORITES (as const), types, interface Tache, classes CreerTacheDto et ModifierTacheDto, interface TodoConfig et jeton TODO_CONFIG.
  2. src/common/horloge.ts: jeton HORLOGE (Symbol), interface Horloge, classe HorlogeSysteme.
  3. src/common/generateur-id.ts: classe abstraite GenerateurId et deux implémentations, GenerateurSequentiel (T-0001, T-0002…) et GenerateurUuid.
  4. src/common/contexte-utilisateur.ts: provider de portée requête qui lit X-Utilisateur.
  5. Commit : feat: modèle et providers communs.
Partie 3 Le service des tâches
  1. TachesService injecte GenerateurId, HORLOGE et TODO_CONFIG. Il ne connaît ni HTTP ni la requête : l'auteur lui est passé en paramètre de create.
  2. Écrivez findAll(filtre), findOne, create, update, terminer, remove, supprimerTerminees et statistiques, avec les règles du cahier des charges (exceptions 400, 404, 409).
  3. Dans update, ne recopiez que les champs autorisés : un client ne doit pas pouvoir modifier id, creePar ou creeLe.
  4. Toutes les dates (creeLe, modifieLe, calcul du retard) viennent de l'horloge injectée, jamais de new Date() directement.
  5. Commit : feat: service des tâches.

Pourquoi une horloge injectée ? Parce qu'un test du calcul « en retard » doit pouvoir fixer la date du jour. Avec new Date() dans le service, le résultat du test changerait selon le jour où il est lancé.

Partie 4 Le module et le contrôleur
  1. Dans TachesModule, déclarez : HORLOGE avec useClass ; GenerateurId avec useClass, GenerateurUuid si process.env.ID_MODE === 'uuid', sinon GenerateurSequentiel; TODO_CONFIG avec useFactory (MAX_TACHES, 100 par défaut ; priorité par défaut « normale ») ; ContexteUtilisateur.
  2. Écrivez TachesController (version 1). Il injecte le service et le contexte, déclare les routes statiques (statistiques, terminees, :id/terminer) avant :id, renvoie Location et X-Total-Count avec passthrough, et répond 204 au DELETE.
  3. Ajoutez SanteController (VERSION_NEUTRAL).
  4. Commit : feat: contrôleur et configuration des providers.
Partie 5 Tester et changer d'implémentation
  1. Créez requetes/taches.http (base ci-dessous) et vérifiez tous les statuts.
  2. Relancez avec ID_MODE=uuid npm run start:dev (sous PowerShell : $env:ID_MODE="uuid"; npm run start:dev) : les identifiants deviennent des UUID. Notez dans le README que le service n'a pas été modifié.
  3. Relancez avec MAX_TACHES=3 et vérifiez le 409.
  4. Commit : test: requêtes de l'API.
@base = http://localhost:3000/api/v1/taches

### Créer (201 + Location)
POST {{base}}
Content-Type: application/json
X-Utilisateur: hajji

{ "titre": "Préparer le TP 4", "priorite": "haute", "echeance": "2026-11-20" }

### Créer sans priorité (priorité par défaut)
POST {{base}}
Content-Type: application/json

{ "titre": "Réviser NestJS", "description": "chapitres 05 à 07" }

### Erreurs : titre vide, priorité invalide, date invalide (400)
POST {{base}}
Content-Type: application/json

{ "titre": "Tâche", "priorite": "critique" }

### Lister (X-Total-Count), filtrer, rechercher
GET {{base}}
###
GET {{base}}?priorite=haute
###
GET {{base}}?q=nestjs

### Statistiques
GET {{base}}/statistiques

### Terminer (409 si déjà terminée)
PATCH {{base}}/T-0001/terminer

### Modifier (id et creePar ignorés)
PATCH {{base}}/T-0002
Content-Type: application/json

{ "statut": "en-cours", "id": "PIRATE" }

### Supprimer les tâches terminées
DELETE {{base}}/terminees

### Supprimer une tâche (204)
DELETE {{base}}/T-0002
Partie 6 Documenter et livrer
  1. Rédigez le README.md: présentation, lancement, variables d'environnement (PORT, ID_MODE, MAX_TACHES), tableau des routes, et un paragraphe « Architecture » qui explique chaque provider (jeton, recette, portée).
  2. Vérifiez npm run lint et npm run build.
  3. Commit docs: README, push, étiquette v1.0.0.
Résultat attendu

Réponses obtenues avec la correction (lancée avec MAX_TACHES=4) :

POST /api/v1/taches → 201 {"id":"T-0001","titre":"Préparer le TP 4","statut":"a-faire","priorite":"haute","echeance":"2026-11-20","creePar":"hajji","creeLe":"…","modifieLe":"…"}  [location: /api/v1/taches/T-0001]
POST /api/v1/taches → 201 {"id":"T-0002","titre":"Corriger les copies","statut":"a-faire","priorite":"normale","echeance":"2026-11-10","creePar":"hajji","creeLe":"…","modifieLe":"…"}
POST /api/v1/taches → 201 {"id":"T-0003","titre":"Réviser NestJS","description":"chapitres 05 à 07","statut":"a-faire","priorite":"normale","creePar":"anonyme","creeLe":"…","modifieLe":"…"}
POST /api/v1/taches → 400 {"message":"Le titre est obligatoire","error":"Bad Request","statusCode":400}
POST /api/v1/taches → 400 {"message":"Priorité invalide : critique (basse, normale, haute)","error":"Bad Request","statusCode":400}
POST /api/v1/taches → 400 {"message":"Échéance invalide : 20/11/2026 (format AAAA-MM-JJ)","error":"Bad Request","statusCode":400}
GET /api/v1/taches → 200 [{"id":"T-0002","titre":"Corriger les copies","statut":"a-faire","priorite":"normale","echeance":"2026-11-10","creePar":"hajji","creeLe":"…","modifieLe":"…"},{"id":"T-0001","titre":"Préparer le TP 4","statut":"a-faire","priorite":"haute","echeance":"2026-11-20","creePar":"hajji","creeLe":"…","modifieLe":"…"},{"id":"T-0003","titre":"Réviser NestJS","description":"chapitres 05 à 07","statut":"a-faire","priorite":"normale","creePar":"anonyme","creeLe":"…","modifieLe":"…"}]  [x-total-count: 3]
GET /api/v1/taches?priorite=haute → 200 [{"id":"T-0001","titre":"Préparer le TP 4","statut":"a-faire","priorite":"haute","echeance":"2026-11-20","creePar":"hajji","creeLe":"…","modifieLe":"…"}]
GET /api/v1/taches?q=nestjs → 200 [{"id":"T-0003","titre":"Réviser NestJS","description":"chapitres 05 à 07","statut":"a-faire","priorite":"normale","creePar":"anonyme","creeLe":"…","modifieLe":"…"}]
GET /api/v1/taches?statut=fini → 400 {"message":"Statut invalide : fini (a-faire, en-cours, terminee)","error":"Bad Request","statusCode":400}
PATCH /api/v1/taches/T-0002/terminer → 200 {"id":"T-0002","titre":"Corriger les copies","statut":"terminee","priorite":"normale","echeance":"2026-11-10","creePar":"hajji","creeLe":"…","modifieLe":"…"}
PATCH /api/v1/taches/T-0002/terminer → 409 {"message":"La tâche T-0002 est déjà terminée","error":"Conflict","statusCode":409}
PATCH /api/v1/taches/T-0003 → 200 {"id":"T-0003","titre":"Réviser NestJS","description":"chapitres 05 à 07","statut":"en-cours","priorite":"normale","creePar":"anonyme","creeLe":"…","modifieLe":"…"}
GET /api/v1/taches/statistiques → 200 {"total":3,"parStatut":{"a-faire":1,"en-cours":1,"terminee":1},"enRetard":0,"avancement":33}
POST /api/v1/taches → 201 {"id":"T-0004","titre":"Quatrième tâche","statut":"a-faire","priorite":"normale","creePar":"anonyme","creeLe":"…","modifieLe":"…"}
POST /api/v1/taches → 409 {"message":"Nombre maximal de tâches atteint (4)","error":"Conflict","statusCode":409}
DELETE /api/v1/taches/terminees → 200 {"supprimees":1}
DELETE /api/v1/taches/T-0001 → 204
GET /api/v1/taches/T-0001 → 404 {"message":"Tâche T-0001 introuvable","error":"Not Found","statusCode":404}
GET /sante → 200 {"statut":"ok","application":"api-todolist"}

Avec ID_MODE=uuid, les mêmes requêtes produisent des identifiants comme d0daba74-7d91-4b88-93a3-41731920e528, sans aucune modification du service.

Pour aller plus loin
  • Écrivez un test avec @nestjs/testing qui fournit une horloge fixée au 2026-11-25 et vérifie que statistiques().enRetard compte la tâche dont l'échéance est le 2026-11-20.
  • Ajoutez une ressource listes (une tâche appartient à une liste) dans un second module, sans créer de dépendance circulaire.
  • Remplacez ContexteUtilisateur (portée requête) par un décorateur de paramètre @Utilisateur() (leçon 5.4) : le contrôleur redevient un singleton. Mesurez la différence avec l'activité de la leçon 7.5.
  • Ajoutez un StockageTaches abstrait avec une implémentation en mémoire et une implémentation dans un fichier JSON (useClass selon STOCKAGE).
Correction
src/taches/tache.model.ts
export const STATUTS = ['a-faire', 'en-cours', 'terminee'] as const;
export type Statut = (typeof STATUTS)[number];

export const PRIORITES = ['basse', 'normale', 'haute'] as const;
export type Priorite = (typeof PRIORITES)[number];

export interface Tache {
  id: string;
  titre: string;
  description?: string;
  statut: Statut;
  priorite: Priorite;
  echeance?: string;          // AAAA-MM-JJ
  creePar: string;
  creeLe: string;
  modifieLe: string;
}

export class CreerTacheDto {
  titre: string;
  description?: string;
  priorite?: Priorite;
  echeance?: string;
}

export class ModifierTacheDto {
  titre?: string;
  description?: string;
  statut?: Statut;
  priorite?: Priorite;
  echeance?: string;
}

export interface TodoConfig {
  maxTaches: number;
  prioriteParDefaut: Priorite;
}

export const TODO_CONFIG = 'TODO_CONFIG';
src/common/horloge.ts
import { Injectable } from '@nestjs/common';

export const HORLOGE = Symbol('HORLOGE');

export interface Horloge {
  maintenant(): Date;
}

@Injectable()
export class HorlogeSysteme implements Horloge {
  maintenant(): Date {
    return new Date();
  }
}
src/common/generateur-id.ts
import { Injectable } from '@nestjs/common';
import { randomUUID } from 'node:crypto';

export abstract class GenerateurId {
  abstract suivant(): string;
}

@Injectable()
export class GenerateurSequentiel extends GenerateurId {
  private compteur = 0;
  suivant(): string {
    return `T-${String(++this.compteur).padStart(4, '0')}`;    // T-0001, T-0002…
  }
}

@Injectable()
export class GenerateurUuid extends GenerateurId {
  suivant(): string {
    return randomUUID();
  }
}
src/common/contexte-utilisateur.ts
import { Inject, Injectable, Scope } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import type { Request } from 'express';

@Injectable({ scope: Scope.REQUEST })
export class ContexteUtilisateur {
  readonly nom: string;

  constructor(@Inject(REQUEST) requete: Request) {
    const entete = requete.headers['x-utilisateur'];
    this.nom = typeof entete === 'string' && entete.trim() ? entete.trim() : 'anonyme';
  }
}
src/taches/taches.service.ts
import { BadRequestException, ConflictException, Inject, Injectable, NotFoundException } from '@nestjs/common';
import { GenerateurId } from '../common/generateur-id';
import { HORLOGE, Horloge } from '../common/horloge';
import {
  CreerTacheDto, ModifierTacheDto, Priorite, PRIORITES, Statut, STATUTS, Tache, TODO_CONFIG, TodoConfig,
} from './tache.model';

export interface FiltreTaches {
  statut?: string;
  priorite?: string;
  q?: string;
}

@Injectable()
export class TachesService {
  private taches: Tache[] = [];

  constructor(
    private readonly generateurId: GenerateurId,
    @Inject(HORLOGE) private readonly horloge: Horloge,
    @Inject(TODO_CONFIG) private readonly config: TodoConfig,
  ) {}

  findAll(filtre: FiltreTaches = {}): Tache[] {
    if (filtre.statut) this.verifierStatut(filtre.statut);
    if (filtre.priorite) this.verifierPriorite(filtre.priorite);
    const q = filtre.q?.toLowerCase();
    return this.taches
      .filter((t) => !filtre.statut || t.statut === filtre.statut)
      .filter((t) => !filtre.priorite || t.priorite === filtre.priorite)
      .filter((t) => !q || `${t.titre} ${t.description ?? ''}`.toLowerCase().includes(q))
      .toSorted((a, b) => (a.echeance ?? '9999').localeCompare(b.echeance ?? '9999'));   // échéance la plus proche d'abord
  }

  findOne(id: string): Tache {
    const t = this.taches.find((x) => x.id === id);
    if (!t) throw new NotFoundException(`Tâche ${id} introuvable`);
    return t;
  }

  create(dto: CreerTacheDto, utilisateur: string): Tache {
    if (!dto?.titre?.trim()) throw new BadRequestException('Le titre est obligatoire');
    if (this.taches.length >= this.config.maxTaches) {
      throw new ConflictException(`Nombre maximal de tâches atteint (${this.config.maxTaches})`);
    }
    if (dto.priorite) this.verifierPriorite(dto.priorite);
    if (dto.echeance) this.verifierDate(dto.echeance);
    const maintenant = this.horloge.maintenant().toISOString();
    const tache: Tache = {
      id: this.generateurId.suivant(),
      titre: dto.titre.trim(),
      description: dto.description,
      statut: 'a-faire',
      priorite: dto.priorite ?? this.config.prioriteParDefaut,
      echeance: dto.echeance,
      creePar: utilisateur,
      creeLe: maintenant,
      modifieLe: maintenant,
    };
    this.taches.push(tache);
    return tache;
  }

  update(id: string, dto: ModifierTacheDto): Tache {
    const t = this.findOne(id);
    if (dto.statut) this.verifierStatut(dto.statut);
    if (dto.priorite) this.verifierPriorite(dto.priorite);
    if (dto.echeance) this.verifierDate(dto.echeance);
    if (dto.titre !== undefined && !dto.titre.trim()) throw new BadRequestException('Le titre ne peut pas être vide');
    const { titre, description, statut, priorite, echeance } = dto;   // seuls les champs autorisés
    Object.assign(t, Object.fromEntries(
      Object.entries({ titre, description, statut, priorite, echeance }).filter(([, v]) => v !== undefined),
    ));
    t.modifieLe = this.horloge.maintenant().toISOString();
    return t;
  }

  terminer(id: string): Tache {
    const t = this.findOne(id);
    if (t.statut === 'terminee') throw new ConflictException(`La tâche ${id} est déjà terminée`);
    return this.update(id, { statut: 'terminee' });
  }

  remove(id: string): void {
    this.findOne(id);
    this.taches = this.taches.filter((t) => t.id !== id);
  }

  supprimerTerminees(): number {
    const avant = this.taches.length;
    this.taches = this.taches.filter((t) => t.statut !== 'terminee');
    return avant - this.taches.length;
  }

  statistiques() {
    const aujourdHui = this.horloge.maintenant().toISOString().slice(0, 10);
    const parStatut = Object.fromEntries(STATUTS.map((s) => [s, this.taches.filter((t) => t.statut === s).length]));
    const enRetard = this.taches.filter((t) => t.statut !== 'terminee' && t.echeance && t.echeance < aujourdHui).length;
    const avancement = this.taches.length ? Math.round((parStatut['terminee'] / this.taches.length) * 100) : 0;
    return { total: this.taches.length, parStatut, enRetard, avancement };
  }

  private verifierStatut(s: string): asserts s is Statut {
    if (!STATUTS.includes(s as Statut)) throw new BadRequestException(`Statut invalide : ${s} (${STATUTS.join(', ')})`);
  }

  private verifierPriorite(p: string): asserts p is Priorite {
    if (!PRIORITES.includes(p as Priorite)) throw new BadRequestException(`Priorité invalide : ${p} (${PRIORITES.join(', ')})`);
  }

  private verifierDate(d: string): void {
    if (!/^\d{4}-\d{2}-\d{2}$/.test(d) || Number.isNaN(Date.parse(d))) {
      throw new BadRequestException(`Échéance invalide : ${d} (format AAAA-MM-JJ)`);
    }
  }
}
src/taches/taches.controller.ts
import {
  Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Patch, Post, Query, Res,
} from '@nestjs/common';
import type { Response } from 'express';
import { ContexteUtilisateur } from '../common/contexte-utilisateur';
import { CreerTacheDto, ModifierTacheDto } from './tache.model';
import { TachesService } from './taches.service';

@Controller({ path: 'taches', version: '1' })
export class TachesController {
  constructor(
    private readonly tachesService: TachesService,
    private readonly contexte: ContexteUtilisateur,       // portée requête
  ) {}

  @Get()
  findAll(
    @Query('statut') statut?: string,
    @Query('priorite') priorite?: string,
    @Query('q') q?: string,
    @Res({ passthrough: true }) res?: Response,
  ) {
    const liste = this.tachesService.findAll({ statut, priorite, q });
    res?.setHeader('X-Total-Count', String(liste.length));
    return liste;
  }

  @Get('statistiques')                                   // avant ':id'
  statistiques() {
    return this.tachesService.statistiques();
  }

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

  @Post()
  create(@Body() dto: CreerTacheDto, @Res({ passthrough: true }) res: Response) {
    const tache = this.tachesService.create(dto, this.contexte.nom);
    res.location(`/api/v1/taches/${tache.id}`);
    return tache;
  }

  @Patch(':id/terminer')
  terminer(@Param('id') id: string) {
    return this.tachesService.terminer(id);
  }

  @Patch(':id')
  update(@Param('id') id: string, @Body() dto: ModifierTacheDto) {
    return this.tachesService.update(id, dto);
  }

  @Delete('terminees')                                   // avant ':id'
  supprimerTerminees() {
    return { supprimees: this.tachesService.supprimerTerminees() };
  }

  @Delete(':id')
  @HttpCode(HttpStatus.NO_CONTENT)
  remove(@Param('id') id: string) {
    this.tachesService.remove(id);
  }
}
src/taches/taches.module.ts
import { Module } from '@nestjs/common';
import { ContexteUtilisateur } from '../common/contexte-utilisateur';
import { GenerateurId, GenerateurSequentiel, GenerateurUuid } from '../common/generateur-id';
import { HORLOGE, HorlogeSysteme } from '../common/horloge';
import { TODO_CONFIG, TodoConfig } from './tache.model';
import { TachesController } from './taches.controller';
import { TachesService } from './taches.service';

@Module({
  controllers: [TachesController],
  providers: [
    TachesService,
    ContexteUtilisateur,
    { provide: HORLOGE, useClass: HorlogeSysteme },
    {
      provide: GenerateurId,
      useClass: process.env.ID_MODE === 'uuid' ? GenerateurUuid : GenerateurSequentiel,
    },
    {
      provide: TODO_CONFIG,
      useFactory: (): TodoConfig => ({
        maxTaches: Number(process.env.MAX_TACHES ?? 100),
        prioriteParDefaut: 'normale',
      }),
    },
  ],
})
export class TachesModule {}
src/sante.controller.ts
import { Controller, Get, VERSION_NEUTRAL } from '@nestjs/common';

@Controller({ path: 'sante', version: VERSION_NEUTRAL })
export class SanteController {
  @Get()
  etat() {
    return { statut: 'ok', application: 'api-todolist' };
  }
}
src/app.module.ts
import { Module } from '@nestjs/common';
import { SanteController } from './sante.controller';
import { TachesModule } from './taches/taches.module';

@Module({
  imports: [TachesModule],
  controllers: [SanteController],
})
export class AppModule {}
src/main.ts
import { Logger, VersioningType } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.setGlobalPrefix('api', { exclude: ['sante'] });
  app.enableVersioning({ type: VersioningType.URI, defaultVersion: '1' });
  app.enableCors({ exposedHeaders: ['Location', 'X-Total-Count'] });

  const port = process.env.PORT ?? 3000;
  await app.listen(port);
  Logger.log(`API TodoList : http://localhost:${port}/api/v1/taches`, 'Bootstrap');
}
bootstrap();
Grille d'évaluation
CritèrePoints
Dépôt GitHub dès la Partie 1, un commit clair par partie, étiquette v1.0.02
Service : CRUD, filtres, tri, statistiques, règles (400, 404, 409), champs protégés en modification6
Providers : jeton Symbol, classe abstraite, useClass selon l'environnement, useFactory, aucun new Date() dans le service5
Contrôleur : routes dans le bon ordre, Location, X-Total-Count, 204, contexte de portée requête4
Fichier .http complet, README avec variables d'environnement et architecture3
Total20