Voici une version complète en français d'un tutoriel approfondi sur NestJS avec PostgreSQL :
Pourquoi NestJS avec PostgreSQL : guide pratique ?
Contexte réel : pourquoi un dev a besoin de ça au quotidien
Un développeur backend doit souvent intégrer des bases de données pour stocker et gérer les données de son application. PostgreSQL est l'un des systèmes de gestion de base de données (SGBD) les plus populaires et performants disponibles aujourd'hui. NestJS, un cadre d'architecture basé sur le principe du microservice, offre une structure solide et flexible pour développer des applications robustes.
Un cas d'utilisation concret en 2-3 phrases
Imaginons que nous voulions créer une application de gestion de tâches. Nous aurions besoin d'une API RESTful pour ajouter, mettre à jour, supprimer et récupérer les tâches. En utilisant NestJS avec PostgreSQL, nous pouvons facilement construire cette application en maintenant une architecture modulaire et scalable.
Prerequis
Connaissances nécessaires :
- JavaScript/TypeScript
- Connaissance de la programmation asynchrone
- Familiarité avec les SGBD (PostgreSQL)
- Connaissance des concepts d'API RESTful
Outils à installer :
- Node.js et npm (>= v12.0.0)
- PostgreSQL (>= 10)
- Yarn (optionnel, mais recommandé)
Concepts fondamentaux
1. Installation et configuration de NestJS
npm install -g @nestjs/cli
nest new task-management-api
cd task-management-api
Structure du projet :
src/app.controller.ts: Point d'entrée de l'application.src/app.service.ts: Service principal de l'application.src/main.ts: Point d'entrée principal.
2. Installation et configuration de PostgreSQL
## Installez PostgreSQL
sudo apt-get update && sudo apt-get install postgresql postgresql-contrib
## Créez un utilisateur et une base de données
sudo -u postgres createuser --interactive
## Choisissez le rôle "createrole" lors de la création
sudo -u postgres createdb task_management_db
Structure de la base de données :
CREATE TABLE tasks (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
description TEXT,
completed BOOLEAN DEFAULT FALSE
);
3. Installation et configuration des dépendances
npm install @nestjs/typeorm typeorm pg @nestjs/common @nestjs/core class-validator class-transformer
Configuration de TypeORM :
Créez un fichier ormconfig.json à la racine du projet :
{
"type": "postgres",
"host": "localhost",
"port": 5432,
"username": "task_management_user",
"password": "your_password",
"database": "task_management_db",
"synchronize": true,
"logging": false,
"entities": ["dist/**/*.entity{.ts,.js}"],
"migrations": ["dist/migrations/*{.ts,.js}"],
"subscribers": []
}
4. Création d'une entité Task
// src/tasks/task.entity.ts
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
@Entity()
export class Task {
@PrimaryGeneratedColumn()
id: number;
@Column()
title: string;
@Column({ nullable: true })
description?: string;
@Column({ default: false })
completed: boolean;
}
5. Création d'un service Task
// src/tasks/task.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Task } from './task.entity';
@Injectable()
export class TasksService {
constructor(
@InjectRepository(Task)
private readonly tasksRepository: Repository<Task>,
) {}
findAll(): Promise<Task[]> {
return this.tasksRepository.find();
}
findOne(id: number): Promise<Task> {
return this.tasksRepository.findOne(id);
}
create(task: Task): Promise<Task> {
return this.tasksRepository.save(task);
}
update(id: number, task: Task): Promise<void> {
task.id = id;
return this.tasksRepository.save(task);
}
delete(id: number): Promise<void> {
await this.tasksRepository.delete(id);
}
}
6. Création d'un contrôleur Task
// src/tasks/task.controller.ts
import { Controller, Get, Post, Put, Delete, Body, Param } from '@nestjs/common';
import { TasksService } from './task.service';
import { Task } from './task.entity';
@Controller('tasks')
export class TasksController {
constructor(private readonly tasksService: TasksService) {}
@Get()
findAll(): Promise<Task[]> {
return this.tasksService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string): Promise<Task> {
return this.tasksService.findOne(+id);
}
@Post()
create(@Body() task: Task): Promise<Task> {
return this.tasksService.create(task);
}
@Put(':id')
update(@Param('id') id: string, @Body() task: Task): Promise<void> {
return this.tasksService.update(+id, task);
}
@Delete(':id')
delete(@Param('id') id: string): Promise<void> {
return this.tasksService.delete(+id);
}
}
Mise en pratique : projet fil rouge
Étape 1 : Initialisation du projet
npm install -g @nestjs/cli
nest new task-management-api
cd task-management-api
Étape 2 : Configuration de PostgreSQL
Suivez les étapes précédentes pour installer et configurer PostgreSQL.
Étape 3 : Installation des dépendances
npm install @nestjs/typeorm typeorm pg @nestjs/common @nestjs/core class-validator class-transformer
Créez un fichier ormconfig.json à la racine du projet :
{
"type": "postgres",
"host": "localhost",
"port": 5432,
"username": "task_management_user",
"password": "your_password",
"database": "task_management_db",
"synchronize": true,
"logging": false,
"entities": ["dist/**/*.entity{.ts,.js}"],
"migrations": ["dist/migrations/*{.ts,.js}"],
"subscribers": []
}
Étape 4 : Création d'une entité Task
// src/tasks/task.entity.ts
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
@Entity()
export class Task {
@PrimaryGeneratedColumn()
id: number;
@Column()
title: string;
@Column({ nullable: true })
description?: string;
@Column({ default: false })
completed: boolean;
}
Étape 5 : Création d'un service Task
// src/tasks/task.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Task } from './task.entity';
@Injectable()
export class TasksService {
constructor(
@InjectRepository(Task)
private readonly tasksRepository: Repository<Task>,
) {}
findAll(): Promise<Task[]> {
return this.tasksRepository.find();
}
findOne(id: number): Promise<Task> {
return this.tasksRepository.findOne(id);
}
create(task: Task): Promise<Task> {
return this.tasksRepository.save(task);
}
update(id: number, task: Task): Promise<void> {
task.id = id;
return this.tasksRepository.save(task);
}
delete(id: number): Promise<void> {
await this.tasksRepository.delete(id);
}
}
Étape 6 : Création d'un contrôleur Task
// src/tasks/task.controller.ts
import { Controller, Get, Post, Put, Delete, Body, Param } from '@nestjs/common';
import { TasksService } from './task.service';
import { Task } from './task.entity';
@Controller('tasks')
export class TasksController {
constructor(private readonly tasksService: TasksService) {}
@Get()
findAll(): Promise<Task[]> {
return this.tasksService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string): Promise<Task> {
return this.tasksService.findOne(+id);
}
@Post()
create(@Body() task: Task): Promise<Task> {
return this.tasksService.create(task);
}
@Put(':id')
update(@Param('id') id: string, @Body() task: Task): Promise<void> {
return this.tasksService.update(+id, task);
}
@Delete(':id')
delete(@Param('id') id: string): Promise<void> {
return this.tasksService.delete(+id);
}
}
Étape 7 : Configuration du module principal
// src/app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TasksController } from './tasks/task.controller';
import { TasksService } from './tasks/task.service';
import { Task } from './tasks/task.entity';
@Module({
imports: [TypeOrmModule.forRoot(), TypeOrmModule.forFeature([Task])],
controllers: [TasksController],
providers: [TasksService],
})
export class AppModule {}
Étape 8 : Exécution de l'application
npm run start
Erreurs frequentes et debugging
Erreur 1 : "QueryFailedError: relation 'tasks' does not exist"
Code incorrect :
// src/tasks/task.repository.ts
import { Repository } from 'typeorm';
import { Task } from './task.entity';
export class TasksRepository extends Repository<Task> {}
Code correct :
// src/tasks/task.repository.ts
import { EntityRepository, Repository } from 'typeorm';
import { Task } from './task.entity';
@EntityRepository(Task)
export class TasksRepository extends Repository<Task> {}
Erreur 2 : "Cannot find module 'pg'"
Code incorrect :
npm install pg
Code correct :
npm install pg --save
Erreur 3 : "TypeError: Cannot read property 'length' of undefined"
Code incorrect :
// src/tasks/task.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Task } from './task.entity';
@Injectable()
export class TasksService {
constructor(
@InjectRepository(Task)
private readonly tasksRepository: Repository<Task>,
) {}
findAll(): Promise<Task[]> {
return this.tasksRepository.find();
}
}
Code correct :
// src/tasks/task.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Task } from './task.entity';
@Injectable()
export class TasksService {
constructor(
@InjectRepository(Task)
private readonly tasksRepository: Repository<Task>,
) {}
findAll(): Promise<Task[]> {
return this.tasksRepository.find();
}
findOne(id: number): Promise<Task> {
return this.tasksRepository.findOne(id);
}
create(task: Task): Promise<Task> {
return this.tasksRepository.save(task);
}
update(id: number, task: Task): Promise<void> {
task.id = id;
return this.tasksRepository.save(task);
}
delete(id: number): Promise<void> {
await this.tasksRepository.delete(id);
}
}
Pour aller plus loin
1. Ajouter des validations avec class-validator et class-transformer
Ajoutez des annotations de validation à vos entités pour vous assurer que les données sont correctement formatées.
// src/tasks/task.entity.ts
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
import { IsNotEmpty, IsString, IsOptional, IsBoolean } from 'class-validator';
@Entity()
export class Task {
@PrimaryGeneratedColumn()
id: number;
@IsNotEmpty()
@IsString()
title: string;
@IsOptional()
@IsString()
description?: string;
@IsBoolean()
completed: boolean;
}
2. Utiliser les intercepteurs pour ajouter des fonctionnalités communes
Créez un intercepteur pour ajouter automatiquement une date de création et de modification à chaque entité.
// src/tasks/created-at.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class CreatedAtInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const now = new Date();
return next.handle().pipe(
tap((data) => {
if (Array.isArray(data)) {
data.forEach((item) => (item.createdAt = now));
} else {
data.createdAt = now;
}
}),
);
}
}
Appliquez l'intercepteur à votre service :
// src/tasks/task.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Task } from './task.entity';
import { CreatedAtInterceptor } from '../shared/created-at.interceptor';
@Injectable()
export class TasksService {
constructor(
@InjectRepository(Task)
private readonly tasksRepository: Repository<Task>,
) {}
findAll(): Promise<Task[]> {
return this.tasksRepository.find();
}
findOne(id: number): Promise<Task> {
return this.tasksRepository.findOne(id);
}
create(@CreatedAtInterceptor() task: Task): Promise<Task> {
return this.tasksRepository.save(task);
}
update(id: number, task: Task): Promise<void> {
task.id = id;
return this.tasksRepository.save(task);
}
delete(id: number): Promise<void> {
await this.tasksRepository.delete(id);
}
}
3. Créer un module pour les tâches
Organisez votre code en modules pour une meilleure structure et maintenabilité.
// src/tasks/task.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TasksController } from './task.controller';
import { TasksService } from './task.service';
import { Task } from './task.entity';
@Module({
imports: [TypeOrmModule.forFeature([Task])],
controllers: [TasksController],
providers: [TasksService],
})
export class TasksModule {}
Ajoutez le module à l'application principale :
// src/app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TasksModule } from './tasks/task.module';
@Module({
imports: [TypeOrmModule.forRoot(), TasksModule],
})
export class AppModule {}
Défi pratique : créer un CLI tool pour gérer les tâches
Créez un CLI tool qui permet de créer, lire, mettre à jour et supprimer des tâches en utilisant NestJS.
nest generate cli task-cli
Ajoutez une commande pour ajouter une tâche :
// src/tasks/cli/task-command.ts
import { Command, Option, injectable } from '@nestjs/cli';
import { TasksService } from '../task.service';
@injectable()
export default class TaskCommand implements Command {
readonly command = 'add';
readonly description = 'Add a new task';
constructor(private readonly tasksService: TasksService) {}
async run(options: any): Promise<any> {
const { title, description } = options;
await this.tasksService.create({ title, description });
console.log('Task added successfully');
}
getOptions(): Option[] {
return [
{
flags: '-t, --title <title>',
required: true,
description: 'The title of the task',
},
{
flags: '-d, --description [description]',
required: false,
description: 'The description of the task',
},
];
}
}
Ajoutez une commande pour lire toutes les tâches :
// src/tasks/cli/task-command.ts
import { Command, Option, injectable } from '@nestjs/cli';
import { TasksService } from '../task.service';
@injectable()
export default class TaskCommand implements Command {
readonly command = 'list';
readonly description = 'List all tasks';
constructor(private readonly tasksService: TasksService) {}
async run(options: any): Promise<any> {
const tasks = await this.tasksService.findAll();
console.log(tasks);
}
getOptions(): Option[] {
return [];
}
}
Ajoutez les commandes au module CLI :
// src/tasks/cli/task.module.ts
import { Module } from '@nestjs/common';
import { TasksService } from '../task.service';
@Module({
providers: [TasksService],
})
export class TaskModule {}
Ajoutez le module CLI à l'application principale :
// src/app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TasksModule } from './tasks/task.module';
import { TaskCliModule } from './tasks/cli/task.module';
@Module({
imports: [TypeOrmModule.forRoot(), TasksModule, TaskCliModule],
})
export class AppModule {}
Enfin, exécutez les commandes :
nest task add -t "Buy groceries" -d "Remember to buy milk and bread"
nest task list
Cela devrait vous donner une bonne base pour créer votre propre CLI tool avec NestJS. Vous pouvez ajouter davantage de fonctionnalités en suivant la même structure.