📖 Manuel
Guide NestJS
1. Bootstrap du projet
npm i -g @nestjs/cli
nest new my-api --strict # TypeScript strict activé d'emblée
cd my-api
nest g resource users # CRUD complet : module + controller + service + dto + entity
Choix d'ORM :
| Cas | ORM recommandé |
|---|---|
| Relations complexes, legacy DB | TypeORM |
| DX moderne, migrations auto | Prisma |
| Multi-DB, unit-of-work strict | MikroORM |
2. Structure modulaire
src/
app.module.ts ← racine
core/ ← CoreModule (logger, config, DB)
shared/ ← SharedModule (guards, pipes, interceptors réutilisables)
users/
users.module.ts
users.controller.ts
users.service.ts
dto/
create-user.dto.ts
update-user.dto.ts
entities/
user.entity.ts
Règle : un feature module déclare et exporte ce dont les autres ont besoin. Ne rien exporter par défaut.
@Module({
imports: [TypeOrmModule.forFeature([User])],
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService], // seulement si d'autres modules en ont besoin
})
export class UsersModule {}
3. Controllers et DTOs
@ApiTags('users')
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Post()
@HttpCode(HttpStatus.CREATED)
create(@Body() dto: CreateUserDto) {
return this.usersService.create(dto);
}
@Get(':id')
findOne(@Param('id', ParseUUIDPipe) id: string) {
return this.usersService.findOneOrFail(id);
}
}
DTO avec validation :
import { IsEmail, IsString, MinLength } from 'class-validator';
export class CreateUserDto {
@IsEmail()
email: string;
@IsString()
@MinLength(8)
password: string;
}
Active le pipe global dans main.ts :
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // strip propriétés non décorées
forbidNonWhitelisted: true,
transform: true, // cast automatique (string → number, etc.)
}),
);
4. Services et injection de dépendances
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private readonly repo: Repository<User>,
private readonly configService: ConfigService,
) {}
async findOneOrFail(id: string): Promise<User> {
const user = await this.repo.findOne({ where: { id } });
if (!user) throw new NotFoundException(`User ${id} not found`);
return user;
}
}
Custom provider (ex : client externe) :
{
provide: 'PAYMENT_CLIENT',
useFactory: (cfg: ConfigService) =>
new PaymentClient(cfg.get('PAYMENT_API_KEY')),
inject: [ConfigService],
}
5. Guards et authentification JWT
// auth.guard.ts
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
// roles.guard.ts
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(ctx: ExecutionContext): boolean {
const roles = this.reflector.getAllAndOverride<string[]>('roles', [
ctx.getHandler(),
ctx.getClass(),
]);
if (!roles) return true;
const { user } = ctx.switchToHttp().getRequest();
return roles.some((r) => user.roles?.includes(r));
}
}
// usage
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('admin')
@Delete(':id')
remove(@Param('id', ParseUUIDPipe) id: string) { ... }
Config Passport JWT dans JwtStrategy :
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(cfg: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: cfg.get('JWT_SECRET'),
});
}
validate(payload: JwtPayload) { return payload; }
}
6. Interceptors et gestion des erreurs
Interceptor de logging/transform :
@Injectable()
export class TransformInterceptor<T>
implements NestInterceptor<T, { data: T; timestamp: string }>
{
intercept(ctx: ExecutionContext, next: CallHandler) {
return next.handle().pipe(
map((data) => ({ data, timestamp: new Date().toISOString() })),
);
}
}
Exception filter global :
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const status =
exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
ctx.getResponse().status(status).json({
statusCode: status,
message: exception instanceof HttpException
? exception.message
: 'Internal server error',
timestamp: new Date().toISOString(),
});
}
}
Enregistrement dans main.ts :
app.useGlobalFilters(new AllExceptionsFilter());
app.useGlobalInterceptors(new TransformInterceptor());
7. Configuration et variables d'environnement
// app.module.ts
ConfigModule.forRoot({
isGlobal: true,
validationSchema: Joi.object({
NODE_ENV: Joi.string().valid('development', 'production', 'test').required(),
DATABASE_URL: Joi.string().required(),
JWT_SECRET: Joi.string().min(32).required(),
}),
}),
8. Tests
// users.service.spec.ts
describe('UsersService', () => {
let service: UsersService;
let repo: jest.Mocked<Repository<User>>;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [
UsersService,
{ provide: getRepositoryToken(User), useValue: createMock<Repository<User>>() },
],
}).compile();
service = module.get(UsersService);
repo = module.get(getRepositoryToken(User));
});
it('throws NotFoundException when user not found', async () => {
repo.findOne.mockResolvedValue(null);
await expect(service.findOneOrFail('abc')).rejects.toThrow(NotFoundException);
});
});
Test e2e :
npm run test:e2e # utilise supertest + app.init()
9. Anti-patterns et pièges
| Anti-pattern | Problème | Correction |
|---|---|---|
| Logique métier dans le controller | Couplage HTTP/métier | Déplacer dans le service |
any sur les corps de requête | Bypass de validation | DTO + ValidationPipe obligatoire |
| Module circulaire | Erreur runtime | forwardRef(() => ModuleB) ou refactorer |
Pas de whitelist: true | Propriétés injectées silencieuses | Toujours activer |
new Service() manuel | Échappe au DI, non testable | Injecter via constructeur |
| Secrets hardcodés | Fuite de sécurité | ConfigService + .env validé par Joi |
| N+1 queries | Perf dégradée | relations dans findOne ou QueryBuilder |
| Pas de graceful shutdown | Requêtes coupées | app.enableShutdownHooks() |
10. Bonnes pratiques 2026
- Versioning API :
app.enableVersioning({ type: VersioningType.URI })+@Version('2')sur le controller. - Rate limiting :
@nestjs/throttlerglobal + override par route avec@Throttle(). - Swagger automatique :
@nestjs/swagger+PluginOptionsdansnest-cli.jsonpour inférer les types sans décorateurs redondants. - Health checks :
@nestjs/terminus— exposer/healthavec checks DB, Redis, mémoire. - Logging structuré : remplacer le logger par défaut par
pinovianestjs-pino(JSON en prod, pretty en dev). - Sécurité :
helmet(), CORS strict,express-rate-limit, et jamaisapp.enableCors()sansoriginexplicite. - Docker : image multi-stage,
node:20-alpine, copier uniquementdist/+node_modulespruned.