NestJS pour dev solo : pourquoi j'ai choisi ce stack
Après avoir testé Express, Fastify et Hono, j'ai choisi NestJS pour l'API de Coachy. Structure opinionnelle, DI native et écosystème : mes raisons concrètes.
NestJS impose une structure (modules, services, controllers) qui semble overkill au début. Après 6 mois sur l'API Coachy, c'est exactement cette structure qui me fait gagner du temps : je sais toujours où chercher, et ajouter une feature ne casse rien.
L'API de Coachy a commencé sur Express.js. Simple, flexible, tout le monde connaît. Trois mois plus tard : un spaghetti code où je passais plus de temps à chercher où était implémentée une feature qu'à coder.
Fastify pour les performances, Hono pour la légèreté — j'ai testé. C'est NestJS qui s'est imposé, malgré sa réputation de "framework trop lourd pour des petits projets".
Spoiler : pour un dev solo qui développe en side-project, l'architecture opinionnelle de NestJS change vraiment la donne.
Pourquoi j'ai abandonné Express
Express, c'est la liberté totale. Et c'est justement le problème. Chaque décision architecturale te revient : où mettre la validation ? Comment organiser les middlewares ? Comment gérer la DI ? Où stocker la config ?
// Express - Liberté totale = anarchie totaleapp.post('/api/workouts', authenticateUser, validateWorkout, (req, res) => {
const workout = req.body;
// Où est la logique métier ?
// Comment tester cette route ?
// Comment gérer les erreurs proprement ?
workoutService.create(workout)
.then(result => res.json(result))
.catch(err => res.status(500).json({error: err.message}));
});
// Fichier séparé pour workoutService... ou pas
// Validation... quelque part
// Config... dans process.env directly
// Tests... on verra plus tard
Au bout de quelques mois, j'avais 15 façons différentes de gérer les erreurs, 3 patterns de validation différents selon l'humeur du jour, et zéro cohérence dans l'architecture. Chaque nouvelle feature était une réinvention de la roue.
NestJS : l'architecture qui scale avec toi
NestJS impose une structure. Au début, ça frustre. Après 6 mois, tu comprends que c'est ta liberté.
// NestJS - Structure claire et cohérente@Controller('workouts')
export class WorkoutsController {
constructor(private workoutsService: WorkoutsService) {}
@Post()
@UseGuards(AuthGuard)
@UsePipes(ValidationPipe)
async create(@Body() createWorkoutDto: CreateWorkoutDto): Promise<WorkoutResponseDto> {
return this.workoutsService.create(createWorkoutDto);
}
}
@Injectable()
export class WorkoutsService {
constructor(
private workoutsRepository: WorkoutsRepository,
private eventBus: EventBus
) {}
async create(dto: CreateWorkoutDto): Promise<WorkoutResponseDto> {
const workout = await this.workoutsRepository.create(dto);
// Event Sourcing integration
await this.eventBus.publish(new WorkoutCreatedEvent(workout.id));
return WorkoutMapper.toDto(workout);
}
}
Chaque partie a sa place. Les DTOs pour la validation, les services pour la logique, les repositories pour la data, les guards pour l'auth. Tu ne te demandes jamais "où je mets ce code ?".
- Architecture opinionnelle : Pas de paralysie du choix, les patterns sont définis
- Dependency Injection native : Testabilité et modularité out-of-the-box
- Écosystème riche : Passport, TypeORM, Swagger, Redis, tout s'intègre facilement
- TypeScript first : Type safety sur toute la stack
- Decorator pattern : Code expressif et lisible
- CLI puissant : Génération de boilerplate automatique
- Courbe d'apprentissage : Concepts Angular/Spring à assimiler
- Overhead initial : Plus lourd qu'Express pour des APIs simples
- Verbosité : Plus de code pour des features basiques
- Performance : Légèrement plus lent qu'Express pur (négligeable en pratique)
Comparatif concret : Express vs NestJS vs Alternatives
J'ai benchmarked les 4 frameworks sur la même API (authentication, CRUD workouts, WebSocket pour le real-time) :
Express.js
- Setup : 2h pour le boilerplate de base
- Ligne de code : 850 pour l'API complète
- Tests : Difficile à tester proprement sans refactoring
- Maintenance : Chaos organisationnel au bout de 3 mois
Fastify
- Setup : 3h (plugins à configurer manuellement)
- Performance : +40% vs Express sur mes benchmarks
- Problème : Écosystème moins riche, plugins parfois bugués
Hono
- Setup : 1h (vraiment minimaliste)
- Performance : Excellente (+60% vs Express)
- Problème : Trop jeune, manque de patterns établis pour des apps complexes
NestJS
- Setup : 4h (apprentissage des concepts)
- Ligne de code : 1200 (plus verbeux mais plus maintenable)
- Tests : Intégration native, mocking facile
- Maintenance : Structure solide qui tient dans le temps
Pour des APIs jetables ou des microservices ultra-simples, Express ou Hono wins. Pour des projets qui vont évoluer sur 2+ ans, NestJS est rentabilisé dès le 3ème mois.
Les features NestJS qui m'ont convaincu
1. Dependency Injection automatique
// Plus besoin de gérer les imports manuellement@Injectable()
export class WorkoutsService {
constructor(
private workoutsRepository: WorkoutsRepository,
private cacheService: CacheService,
private eventBus: EventBus,
private configService: ConfigService
) {
// NestJS résout automatiquement toutes les dépendances
// Testabilité native avec mocks
}
}
// En test, super simple à mocker
const mockRepository = {
create: jest.fn(),
findById: jest.fn()
};
Test.createTestingModule({
providers: [
WorkoutsService,
{ provide: WorkoutsRepository, useValue: mockRepository }
]
});
2. Validation déclarative avec class-validator
export class CreateWorkoutDto { @IsString()
@Length(3, 50)
name: string;
@IsArray()
@ValidateNested({ each: true })
@Type(() => ExerciseDto)
exercises: ExerciseDto[];
@IsOptional()
@IsDateString()
scheduledAt?: string;
}
// Validation automatique sur toutes les routes
// Erreurs normalisées et i18n-ready
3. Intégration WebSocket native
Pour Coachy, j'ai besoin de sync real-time pendant les séances d'entraînement :
@WebSocketGateway({ cors: true })export class WorkoutGateway {
@WebSocketServer() server: Server;
@SubscribeMessage('joinWorkout')
handleJoinWorkout(client: Socket, workoutId: string) {
client.join(workout-${workoutId});
}
@SubscribeMessage('exerciseCompleted')
async handleExerciseCompleted(
client: Socket,
@MessageBody() data: ExerciseCompletedDto
) {
await this.workoutsService.completeExercise(data);
// Broadcast à tous les clients de cette séance
this.server.to(workout-${data.workoutId}).emit('exerciseUpdated', data);
}
}
4. Swagger intégré
Documentation API générée automatiquement à partir des DTOs et decorators :
@ApiTags('workouts')@ApiBearerAuth()
@Controller('workouts')
export class WorkoutsController {
@ApiOperation({ summary: 'Create a new workout' })
@ApiResponse({ status: 201, type: WorkoutResponseDto })
@Post()
async create(@Body() dto: CreateWorkoutDto) {
// Swagger génère automatiquement la doc
// Avec les schemas, exemples et codes d'erreur
}
}
Performance : le mythe de la lourdeur
"NestJS c'est lourd" revient souvent. En pratique, sur l'API Coachy en développement :
- Temps de démarrage : 2.3s (vs 0.8s pour Express)
- Throughput : 8500 req/s (vs 12000 req/s pour Express)
- Mémoire : +15MB baseline
Pour une API backend typique, ces différences sont invisibles. Le bottleneck sera ta base de données, pas le framework.
Et côté DX (Developer Experience), NestJS gagne haut la main :
# Génération automatique de boilerplatenest g controller workouts
nest g service workouts
nest g module workouts
Tests intégrés
npm run test:watch
npm run test:e2e
Build et déploiement
npm run build
npm run start:prod
Écosystème et intégrations
Ce qui m'a fait rester sur NestJS, c'est son écosystème mature :
Database : TypeORM, Prisma, Mongoose - intégration native
Auth : Passport avec 500+ stratégies
Caching : Redis, Memcached - decorators simples
Validation : class-validator - déclaratif et puissant
Documentation : Swagger/OpenAPI - automatique
Testing : Jest - configuration zero
Monitoring : Prometheus, Grafana - métriques out-of-the-box
Chaque intégration est pensée pour NestJS. Pas de bidouillage, pas de wrapper custom. Ça marche.
Le piège de la sur-architecture
NestJS peut pousser à la sur-architecture. J'ai failli tomber dans le piège :
// Piège : trop de layers pour rien@Controller('users')
export class UsersController {
constructor(private usersApplicationService: UsersApplicationService) {}
}
@Injectable()
export class UsersApplicationService {
constructor(private usersDomainService: UsersDomainService) {}
}
@Injectable()
export class UsersDomainService {
constructor(private usersRepository: UsersRepository) {}
}
// Pour un simple CRUD, c'est du overkill
Ma règle maintenant : commence simple (Controller + Service + Repository), complexifie seulement quand nécessaire. NestJS permet les deux approches.
NestJS brille sur les projets moyens à grands. Pour des microservices ultra-simples ou des scripts one-shot, Express ou Fastify restent plus appropriés.
Retour d'expérience
L'API Coachy est développée avec NestJS depuis le début du projet. Bilan :
Ce qui marche :
- Architecture qui a tenu l'ajout de 15 nouveaux endpoints
- Tests faciles à écrire et maintenir
- Onboarding rapide de contributeurs (architecture claire)
- Performance largement suffisante (P95 < 200ms)
Ce qui m'a surpris :
- Hot reload très efficace en dev
- Debugging excellent avec les decorators
- Communauté active et réactive sur les issues
Ce qui me manque :
- Startup time plus lent en dev (mais acceptable)
- Bundle size plus gros (compensé par les features)
Au final, NestJS m'a fait gagner en vélocité sur le moyen terme. Les 2 premières semaines d'apprentissage ont été rentabilisées dès le 2ème mois de développement.
À lire aussi
- Event Sourcing dans une app mobile : retour d'expérience
- Studio d'un seul homme : mon setup dev
- Build vs Buy pour les startups : quand développer, quand acheter
Sources :