| name | dev-nestjs-guide |
| description | Développement backend NestJS avec modules, controllers, providers, pipes, guards et interceptors. Se déclenche avec "NestJS", "Nest.js", "module NestJS", "controller NestJS", "decorator NestJS". Also triggers on "NestJS module", "Nest guards and interceptors", "dependency injection in Nest". |
Guide NestJS
1. Bootstrap du projet
npm i -g @nestjs/cli
nest new my-api --strict
cd my-api
nest g resource users
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],
})
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,
forbidNonWhitelisted: true,
transform: true,
}),
);
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
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
@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));
}
}
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('admin')
@Delete()
() { ... }
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
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
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
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/throttler global + override par route avec @Throttle().
- Swagger automatique :
@nestjs/swagger + PluginOptions dans nest-cli.json pour inférer les types sans décorateurs redondants.
- Health checks :
@nestjs/terminus — exposer /health avec checks DB, Redis, mémoire.
- Logging structuré : remplacer le logger par défaut par
pino via nestjs-pino (JSON en prod, pretty en dev).
- Sécurité :
helmet(), CORS strict, express-rate-limit, et jamais app.enableCors() sans origin explicite.
- Docker : image multi-stage,
node:20-alpine, copier uniquement dist/ + node_modules pruned.
Communication Rules — MANDATORY
- Ultra-concise. No filler, no preamble, no pleasantries.
- Never say "happy to help", "sure!", "great question", "let me", or similar.
- Tool first, talk second. Act before explaining.
- Result first. Lead with outcome, not process.
- Stop when done. No summary, no recap, no trailing commentary.
- No politeness wrappers. Direct and blunt.
- Minimum words. If one word works, do not use ten.
- No unsolicited explanations.
- No emoji unless asked.