Exercices — Module 01

TypeScript & NestJS Bootstrap · 5 exercices pratiques

Ex 01 ⭐ Facile

Types et interfaces TypeScript

Créez les types TypeScript pour un systÚme de blog : interfaces Post, Author, Category, et les types utilitaires CreatePostDto et PostSummary.

Voir la solution
interface Author {
  readonly id: number;
  name: string;
  email: string;
  bio?: string;
}

interface Category {
  readonly id: number;
  name: string;
  slug: string;
}

interface Post {
  readonly id: number;
  title: string;
  content: string;
  slug: string;
  published: boolean;
  publishedAt?: Date;
  author: Author;
  categories: Category[];
  tags: string[];
  createdAt: Date;
  updatedAt: Date;
}

type CreatePostDto = Omit<Post, 'id' | 'createdAt' | 'updatedAt' | 'author'> & {
  authorId: number;
};

type PostSummary = Pick<Post, 'id' | 'title' | 'slug' | 'published' | 'createdAt'> & {
  authorName: string;
};
Ex 02 ⭐ Facile

Décorateur personnalisé

CrĂ©ez un dĂ©corateur de paramĂštre @IpAddress() qui extrait l'adresse IP de la requĂȘte entrante, et un dĂ©corateur @Public() pour marquer des routes comme publiques.

Voir la solution
// ip-address.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const IpAddress = createParamDecorator(
  (data: unknown, ctx: ExecutionContext): string => {
    const request = ctx.switchToHttp().getRequest();
    return request.ip || request.headers['x-forwarded-for'] || '0.0.0.0';
  },
);

// public.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);

// Usage
@Get('info')
getInfo(@IpAddress() ip: string) {
  return { ip, timestamp: new Date() };
}

@Get('public')
@Public()
getPublicData() {
  return { message: 'Accessible sans authentification' };
}
Ex 03 ⭐⭐ Moyen

Créer un projet NestJS complet

Générez un nouveau projet NestJS avec la CLI, ajoutez la configuration des variables d'environnement (.env + ConfigModule), et configurez la ValidationPipe globale avec whitelist et transform.

Voir la solution
nest new blog-api
cd blog-api
npm install @nestjs/config class-validator class-transformer joi
// main.ts
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';
import { ConfigService } from '@nestjs/config';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.useGlobalPipes(new ValidationPipe({
    whitelist: true,
    forbidNonWhitelisted: true,
    transform: true,
    transformOptions: { enableImplicitConversion: true },
  }));

  app.setGlobalPrefix('api/v1');
  app.enableCors({ origin: process.env.CORS_ORIGIN || '*' });

  const config = app.get(ConfigService);
  const port = config.get<number>('PORT', 3000);
  await app.listen(port);
  console.log(`Application running on: http://localhost:${port}/api/v1`);
}
bootstrap();
Ex 04 ⭐⭐ Moyen

Classe générique CrudService

Implémentez une classe abstraite générique CrudService<T, CreateDto, UpdateDto> avec les méthodes CRUD de base. Implémentez-la dans un PostsService.

Voir la solution
abstract class CrudService<T extends { id: number }, C, U> {
  protected abstract repo: Repository<T>;

  findAll(): Promise<T[]> {
    return this.repo.find();
  }

  async findOne(id: number): Promise<T> {
    const entity = await this.repo.findOneBy({ id } as any);
    if (!entity) throw new NotFoundException(`Entity #${id} not found`);
    return entity;
  }

  async create(dto: C): Promise<T> {
    const entity = this.repo.create(dto as any);
    return this.repo.save(entity);
  }

  async update(id: number, dto: U): Promise<T> {
    await this.findOne(id);
    await this.repo.update(id, dto as any);
    return this.findOne(id);
  }

  async remove(id: number): Promise<void> {
    await this.findOne(id);
    await this.repo.delete(id);
  }
}

@Injectable()
export class PostsService extends CrudService<Post, CreatePostDto, UpdatePostDto> {
  constructor(
    @InjectRepository(Post)
    protected repo: Repository<Post>,
  ) {
    super();
  }
}
Ex 05 ⭐⭐⭐ Difficile

Configuration typée avec factory

Créez une configuration typée avec registerAs pour séparer les configs (database, jwt, mail). Utilisez un schema Joi pour la validation. Injectez la config dans un service.

Voir la solution
// config/database.config.ts
import { registerAs } from '@nestjs/config';

export default registerAs('database', () => ({
  url: process.env.DATABASE_URL,
  host: process.env.DB_HOST || 'localhost',
  port: parseInt(process.env.DB_PORT, 10) || 5432,
  username: process.env.DB_USER || 'postgres',
  password: process.env.DB_PASS || 'postgres',
  database: process.env.DB_NAME || 'nestjs_db',
}));

// Injection dans un service
@Injectable()
export class DatabaseService {
  constructor(
    @Inject(databaseConfig.KEY)
    private dbConfig: ConfigType<typeof databaseConfig>,
  ) {}

  getConnectionString(): string {
    const { username, password, host, port, database } = this.dbConfig;
    return `postgresql://${username}:${password}@${host}:${port}/${database}`;
  }
}
← Cours ▶ Mini-projet Module 02 →