Pourquoi NestJS avec MongoDB : guide pratique ?
NestJS est un framework JavaScript/TypeScript basé sur les principes de la conception orientée objet et conçu pour aider les développeurs à créer des applications robustes, scalables et maintenables. L'intégration de MongoDB, une base de données NoSQL polyvalente, offre une solution efficace pour stocker et gérer les données complexes. Dans ce guide pratique, nous explorerons comment utiliser NestJS avec MongoDB pour construire une application complète.
Contexte réel : Un développeur backend peut avoir besoin d'une application qui nécessite un stockage de données dynamique, évoluant constamment et capable de traiter des volumes importants de données. MongoDB est idéal pour ce type de cas, offrant une performance élevée et une flexibilité dans la façon dont les données sont structurées.
Un cas d'usage concret en 2-3 phrases : Imaginez que vous travaillez sur un service e-commerce. Vous avez besoin d'une base de données capable de stocker des produits, des utilisateurs, des commandes et leurs détails. MongoDB offre une solution flexible qui peut s'échaler facilement avec la croissance de votre base d'utilisateurs.
Prerequis
Pour suivre ce tutoriel, vous aurez besoin des éléments suivants :
- Node.js : >= 14.x
- npm ou yarn : >= 6.x
- MongoDB : >= 4.0 (pour l'environnement local)
- Docker : Facultatif, pour exécuter MongoDB en conteneur
Concepts fondamentaux
1. Installation de NestJS et MongoDB
Tout d'abord, nous devons installer NestJS et les dépendances nécessaires.
npm install -g @nestjs/cli
nest new task-manager-api
cd task-manager-api
Ajoutez MongoDB en tant que dépendance :
npm install mongoose
2. Configuration de la connexion à MongoDB
Créez un fichier config/mongoose.config.ts pour configurer la connexion à MongoDB.
// src/config/mongoose.config.ts
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
@Module({
imports: [
MongooseModule.forRootAsync({
useFactory: async () => ({
uri: 'mongodb://localhost:27017/task-manager', // Change this URI for production
useNewUrlParser: true,
useUnifiedTopology: true,
}),
}),
],
})
export class MongooseConfigModule {}
3. Création d'un modèle Mongoose
Créez un modèle pour les tâches.
// src/tasks/task.schema.ts
import { Schema, Document } from 'mongoose';
export const TaskSchema = new Schema({
title: {
type: String,
required: true,
},
description: {
type: String,
default: '',
},
completed: {
type: Boolean,
default: false,
},
});
export interface Task extends Document {
readonly id: string;
title: string;
description?: string;
completed: boolean;
}
4. Création d'un service pour les tâches
// src/tasks/task.service.ts
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { Task, TaskSchema } from './task.schema';
@Injectable()
export class TasksService {
constructor(@InjectModel(Task.name) private readonly taskModel: Model<Task>) {}
async create(taskDto: Partial<Task>): Promise<Task> {
const createdTask = new this.taskModel(taskDto);
return createdTask.save();
}
async findAll(): Promise<Task[]> {
return this.taskModel.find().exec();
}
async findOne(id: string): Promise<Task> {
return this.taskModel.findById(id).exec();
}
async update(id: string, taskDto: Partial<Task>): Promise<Task> {
return this.taskModel.findByIdAndUpdate(id, taskDto, { new: true }).exec();
}
async remove(id: string): Promise<void> {
await this.taskModel.deleteOne({ _id: id }).exec();
}
}
5. Création d'un controller pour les tâches
// src/tasks/task.controller.ts
import { Controller, Get, Post, Body, Param, Put, Delete } from '@nestjs/common';
import { TasksService } from './task.service';
import { Task } from './task.schema';
@Controller('tasks')
export class TasksController {
constructor(private readonly tasksService: TasksService) {}
@Post()
async create(@Body() taskDto: Partial<Task>): Promise<Task> {
return this.tasksService.create(taskDto);
}
@Get()
async findAll(): Promise<Task[]> {
return this.tasksService.findAll();
}
@Get(':id')
async findOne(@Param('id') id: string): Promise<Task> {
return this.tasksService.findOne(id);
}
@Put(':id')
async update(@Param('id') id: string, @Body() taskDto: Partial<Task>): Promise<Task> {
return this.tasksService.update(id, taskDto);
}
@Delete(':id')
async remove(@Param('id') id: string): Promise<void> {
return this.tasksService.remove(id);
}
}
6. Importation des modules
// src/app.module.ts
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { TasksController } from './tasks/task.controller';
import { TasksService } from './tasks/task.service';
import { TaskSchema, Task } from './tasks/task.schema';
import { MongooseConfigModule } from './config/mongoose.config';
@Module({
imports: [
MongooseConfigModule,
MongooseModule.forFeature([{ name: Task.name, schema: TaskSchema }]),
],
controllers: [TasksController],
providers: [TasksService],
})
export class AppModule {}
Mise en pratique : projet fil rouge
Étape 1 : Initialisation du projet
nest new task-manager-api
cd task-manager-api
npm install mongoose
Étape 2 : Configuration de la connexion à MongoDB
Créez src/config/mongoose.config.ts :
// src/config/mongoose.config.ts
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
@Module({
imports: [
MongooseModule.forRootAsync({
useFactory: async () => ({
uri: 'mongodb://localhost:27017/task-manager',
useNewUrlParser: true,
useUnifiedTopology: true,
}),
}),
],
})
export class MongooseConfigModule {}
Étape 3 : Création d'un modèle Mongoose
Créez src/tasks/task.schema.ts :
// src/tasks/task.schema.ts
import { Schema, Document } from 'mongoose';
export const TaskSchema = new Schema({
title: {
type: String,
required: true,
},
description: {
type: String,
default: '',
},
completed: {
type: Boolean,
default: false,
},
});
export interface Task extends Document {
readonly id: string;
title: string;
description?: string;
completed: boolean;
}
Étape 4 : Création d'un service pour les tâches
Créez src/tasks/task.service.ts :
// src/tasks/task.service.ts
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { Task, TaskSchema } from './task.schema';
@Injectable()
export class TasksService {
constructor(@InjectModel(Task.name) private readonly taskModel: Model<Task>) {}
async create(taskDto: Partial<Task>): Promise<Task> {
const createdTask = new this.taskModel(taskDto);
return createdTask.save();
}
async findAll(): Promise<Task[]> {
return this.taskModel.find().exec();
}
async findOne(id: string): Promise<Task> {
return this.taskModel.findById(id).exec();
}
async update(id: string, taskDto: Partial<Task>): Promise<Task> {
return this.taskModel.findByIdAndUpdate(id, taskDto, { new: true }).exec();
}
async remove(id: string): Promise<void> {
await this.taskModel.deleteOne({ _id: id }).exec();
}
}
Étape 5 : Création d'un controller pour les tâches
Créez src/tasks/task.controller.ts :
// src/tasks/task.controller.ts
import { Controller, Get, Post, Body, Param, Put, Delete } from '@nestjs/common';
import { TasksService } from './task.service';
import { Task } from './task.schema';
@Controller('tasks')
export class TasksController {
constructor(private readonly tasksService: TasksService) {}
@Post()
async create(@Body() taskDto: Partial<Task>): Promise<Task> {
return this.tasksService.create(taskDto);
}
@Get()
async findAll(): Promise<Task[]> {
return this.tasksService.findAll();
}
@Get(':id')
async findOne(@Param('id') id: string): Promise<Task> {
return this.tasksService.findOne(id);
}
@Put(':id')
async update(@Param('id') id: string, @Body() taskDto: Partial<Task>): Promise<Task> {
return this.tasksService.update(id, taskDto);
}
@Delete(':id')
async remove(@Param('id') id: string): Promise<void> {
return this.tasksService.remove(id);
}
}
Étape 6 : Importation des modules
Créez src/app.module.ts :
// src/app.module.ts
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { TasksController } from './tasks/task.controller';
import { TasksService } from './tasks/task.service';
import { TaskSchema, Task } from './tasks/task.schema';
import { MongooseConfigModule } from './config/mongoose.config';
@Module({
imports: [
MongooseConfigModule,
MongooseModule.forFeature([{ name: Task.name, schema: TaskSchema }]),
],
controllers: [TasksController],
providers: [TasksService],
})
export class AppModule {}
Étape 7 : Lancement de l'application
npm run start
Accédez à http://localhost:3000/tasks pour voir les routes disponibles.
Erreurs fréquentes et debugging
1. Erreur : Connection to MongoDB failed
Code incorrect :
// src/config/mongoose.config.ts
MongooseModule.forRootAsync({
useFactory: async () => ({
uri: 'mongodb://localhost:27017/task-manager',
useNewUrlParser: true,
useUnifiedTopology: true,
}),
}),
Code correct :
// src/config/mongoose.config.ts
MongooseModule.forRoot('mongodb://localhost:27017/task-manager', {
useNewUrlParser: true,
useUnifiedTopology: true,
}),
2. Erreur : Cannot read property 'save' of undefined
Code incorrect :
// src/tasks/task.service.ts
async create(taskDto: Partial<Task>): Promise<Task> {
const createdTask = new this.taskModel(taskDto);
return await createdTask.save();
}
Code correct :
// src/tasks/task.service.ts
async create(taskDto: Partial<Task>): Promise<Task> {
const createdTask = new this.taskModel(taskDto);
return createdTask.save();
}
3. Erreur : Validation failed
Code incorrect :
// src/tasks/task.schema.ts
title: {
type: String,
required: true,
},
description: {
type: String,
default: '',
},
completed: {
type: Boolean,
default: false,
},
Code correct :
// src/tasks/task.schema.ts
title: {
type: String,
required: true,
minlength: 1,
},
description: {
type: String,
maxlength: 255,
},
completed: {
type: Boolean,
default: false,
},
Pour aller plus loin
1. Authentification et sécurité
Intégrez une authentification JWT pour sécuriser vos routes.
- Concepts avancés : Authentication with JWT
- Lien : https://docs.nestjs.com/security/authentication
2. Migrations de base de données
Utilisez des outils comme mongoose-migrate pour gérer les migrations de base de données.
- Concepts avancés : Database migrations
- Lien : https://github.com/juliangruber/mongoose-migrate
3. Tests unitaires et d'intégration
Écrivez des tests unitaires et d'intégration pour vos services et contrôleurs.
- Concepts avancés : Testing in NestJS
- Lien : https://docs.nestjs.com/techniques/testing
Défi pratique : Ajout de l'authentification JWT
Ajoutez une fonctionnalité d'authentification JWT à votre application pour sécuriser les routes sensibles.
- Installez
@nestjs/jwtetjsonwebtoken.
npm install @nestjs/jwt jsonwebtoken
- Créez un module pour l'authentification.
// src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
@Module({
imports: [
JwtModule.registerAsync({
useFactory: async () => ({
secret: 'your-secret-key',
signOptions: { expiresIn: '60s' },
}),
}),
],
})
export class AuthModule {}
- Créez un service pour gérer les tokens JWT.
// src/auth/auth.service.ts
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
@Injectable()
export class AuthService {
constructor(private readonly jwtService: JwtService) {}
async generateToken(userId: string): Promise<string> {
return this.jwtService.signAsync({ userId });
}
}
- Ajoutez un middleware pour la vérification des tokens JWT.
// src/auth/jwt.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { JwtService } from '@nestjs/jwt';
@Injectable()
export class JwtMiddleware implements NestMiddleware {
constructor(private readonly jwtService: JwtService) {}
async use(req: Request, res: Response, next: NextFunction) {
const token = req.headers['authorization']?.split(' ')[1];
if (token) {
try {
const decoded = this.jwtService.verify(token);
req.user = decoded;
} catch {}
}
next();
}
}
- Ajoutez le middleware à votre application.
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { JwtMiddleware } from './auth/jwt.middleware';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.use(JwtMiddleware);
await app.listen(3000);
}
bootstrap();
- Protégez une route.
// src/tasks/task.controller.ts
@UseGuards(AuthGuard('jwt'))
@Controller('tasks')
export class TasksController {
constructor(private readonly tasksService: TasksService) {}
@Post()
async create(@Body() taskDto: Partial<Task>, @Request() req): Promise<Task> {
return this.tasksService.create(taskDto);
}
// ... autres routes
}
Lien : https://docs.nestjs.com/security/authentication
Conclusion
Ce guide pratique vous a montré comment créer une application NestJS avec une base de données MongoDB. Vous avez appris à configurer la connexion à la base de données, à créer des modèles Mongoose, et à manipuler les données via des services et contrôleurs. Vous avez également exploré des concepts avancés comme l'authentification JWT et la sécurité des routes.
N'hésitez pas à explorer les ressources supplémentaires pour approfondir votre compréhension de NestJS et d'autres frameworks Node.js.