Exercices — Module 02

Controllers, Routes & Providers · 5 exercices pratiques

Ex 01 ⭐ Facile

Controller REST complet

Créez un BooksController avec les 5 méthodes CRUD : GET /books, GET /books/:id, POST /books, PATCH /books/:id, DELETE /books/:id. Utilisez ParseIntPipe et les codes HTTP appropriés.

Voir la solution
@Controller('books')
export class BooksController {
  constructor(private readonly booksService: BooksService) {}

  @Get()
  findAll(@Query('genre') genre?: string, @Query('author') author?: string) {
    return this.booksService.findAll({ genre, author });
  }

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

  @Post()
  @HttpCode(HttpStatus.CREATED)
  create(@Body() dto: CreateBookDto) {
    return this.booksService.create(dto);
  }

  @Patch(':id')
  update(
    @Param('id', ParseIntPipe) id: number,
    @Body() dto: UpdateBookDto,
  ) {
    return this.booksService.update(id, dto);
  }

  @Delete(':id')
  @HttpCode(HttpStatus.NO_CONTENT)
  remove(@Param('id', ParseIntPipe) id: number) {
    return this.booksService.remove(id);
  }
}
Ex 02 ⭐ Facile

Paramètres avancés

Créez une route de recherche avec pagination : GET /books/search?q=string&page=1&limit=10&sortBy=title&order=asc. Gérez les valeurs par défaut avec DefaultValuePipe.

Voir la solution
@Get('search')
search(
  @Query('q') q = '',
  @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
  @Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit: number,
  @Query('sortBy', new DefaultValuePipe('title')) sortBy: string,
  @Query('order', new DefaultValuePipe('asc')) order: 'asc' | 'desc',
) {
  return this.booksService.search({ q, page, limit, sortBy, order });
}
Ex 03 ⭐⭐ Moyen

Service avec logique métier

Implémentez un BooksService avec un tableau en mémoire (pas de DB). Gérez les cas d'erreur avec les exceptions NestJS appropriées (NotFoundException, ConflictException).

Voir la solution
@Injectable()
export class BooksService {
  private books: Book[] = [];
  private nextId = 1;

  findAll(filters?: Partial<Book>): Book[] {
    return this.books.filter(b =>
      (!filters?.genre || b.genre === filters.genre) &&
      (!filters?.author || b.author.includes(filters.author))
    );
  }

  findOne(id: number): Book {
    const book = this.books.find(b => b.id === id);
    if (!book) throw new NotFoundException(`Book #${id} not found`);
    return book;
  }

  create(dto: CreateBookDto): Book {
    const existing = this.books.find(b => b.isbn === dto.isbn);
    if (existing) throw new ConflictException(`ISBN ${dto.isbn} already exists`);
    const book: Book = { id: this.nextId++, ...dto };
    this.books.push(book);
    return book;
  }

  update(id: number, dto: UpdateBookDto): Book {
    const index = this.books.findIndex(b => b.id === id);
    if (index === -1) throw new NotFoundException(`Book #${id} not found`);
    this.books[index] = { ...this.books[index], ...dto };
    return this.books[index];
  }

  remove(id: number): void {
    const index = this.books.findIndex(b => b.id === id);
    if (index === -1) throw new NotFoundException(`Book #${id} not found`);
    this.books.splice(index, 1);
  }
}
Ex 04 ⭐⭐ Moyen

Value Provider et injection

Créez un provider de configuration 'APP_CONFIG' avec useValue. Injectez-le dans le service via @Inject() et utilisez-le pour configurer les limites de pagination.

Voir la solution
// books.module.ts
@Module({
  controllers: [BooksController],
  providers: [
    BooksService,
    {
      provide: 'APP_CONFIG',
      useValue: {
        maxPageSize: 50,
        defaultPageSize: 10,
        apiVersion: 'v1',
      },
    },
  ],
})
export class BooksModule {}

// books.service.ts
@Injectable()
export class BooksService {
  constructor(
    @Inject('APP_CONFIG')
    private readonly config: { maxPageSize: number; defaultPageSize: number },
  ) {}

  paginate(items: Book[], page: number, rawLimit: number): Book[] {
    const limit = Math.min(rawLimit, this.config.maxPageSize);
    const start = (page - 1) * limit;
    return items.slice(start, start + limit);
  }
}
Ex 05 ⭐⭐⭐ Difficile

Controller avec upload de fichier

Ajoutez une route POST /books/:id/cover pour uploader une image de couverture. Utilisez @UseInterceptors(FileInterceptor) de multer, validez le type MIME (image seulement) et la taille max (2MB).

Voir la solution
npm install @nestjs/platform-express multer @types/multer
import { FileInterceptor } from '@nestjs/platform-express';
import { UploadedFile } from '@nestjs/common';
import { diskStorage } from 'multer';
import { extname } from 'path';

@Post(':id/cover')
@UseInterceptors(FileInterceptor('cover', {
  storage: diskStorage({
    destination: './uploads/covers',
    filename: (req, file, cb) => {
      const uniqueName = `${Date.now()}-${Math.round(Math.random() * 1e9)}`;
      cb(null, uniqueName + extname(file.originalname));
    },
  }),
  limits: { fileSize: 2 * 1024 * 1024 }, // 2 MB
  fileFilter: (req, file, cb) => {
    if (!file.mimetype.startsWith('image/')) {
      return cb(new BadRequestException('Only image files allowed'), false);
    }
    cb(null, true);
  },
}))
async uploadCover(
  @Param('id', ParseIntPipe) id: number,
  @UploadedFile() file: Express.Multer.File,
) {
  const url = `/uploads/covers/${file.filename}`;
  return this.booksService.updateCoverUrl(id, url);
}
← Cours ▶ Mini-projet Module 03 →