Aller au contenu
NestJS OpenPartie 5, section f
Changer de langue : lire en anglais

f · Aller plus loin : Prisma

  • Environ 60 minutes
  • 3 exercices

TypeORM décrit tes tables avec des classes décorées. Prisma part d’une autre idée : un fichier de schéma, écrit dans son propre langage, dont il génère un client typé. Cette section refait le Loup-Garou de la section c avec Prisma, pour que tu reconnaisses son style quand tu le croiseras ailleurs. Le cours a été écrit avec Prisma 7 : on fixe la version majeure à l’installation.

Fais l’essai dans un projet NestJS séparé, avec sa propre base — deux ORM ne doivent pas se disputer les mêmes tables.

Fenêtre de terminal
npm install @prisma/client@7 @prisma/adapter-pg@7 pg @nestjs/config
npm install --save-dev prisma@7

L’adaptateur @prisma/adapter-pg permet à Prisma de parler à PostgreSQL à travers le pilote pg, que tu connais déjà. L’URL de la base va dans le .env :

.env
DATABASE_URL="postgresql://loupgarou:loupgarou@localhost:5432/loupgarou"

La CLI de Prisma lit sa configuration dans prisma.config.ts, à la racine :

prisma.config.ts
import { defineConfig } from 'prisma/config';
process.loadEnvFile();
export default defineConfig({
schema: 'prisma/schema.prisma',
migrations: { path: 'prisma/migrations' },
datasource: { url: process.env.DATABASE_URL },
});

Les trois tables de la section c s’écrivent dans prisma/schema.prisma, sans une ligne de TypeScript :

prisma/schema.prisma
generator client {
provider = "prisma-client"
output = "../src/generated/prisma"
}
datasource db {
provider = "postgresql"
}
model Utilisateur {
id Int @id @default(autoincrement())
pseudo String
participations Participation[]
}
model Village {
id Int @id @default(autoincrement())
nom String
participations Participation[]
}
model Participation {
id Int @id @default(autoincrement())
role String
vivant Boolean @default(true)
village Village @relation(fields: [villageId], references: [id], onDelete: Cascade)
villageId Int
utilisateur Utilisateur @relation(fields: [utilisateurId], references: [id])
utilisateurId Int
}

Tout ce que tu as écrit avec des décorateurs a son équivalent : @id @default(autoincrement()) est la clé primaire générée, @default(true) la valeur par défaut, onDelete: Cascade le même comportement qu’en section c. Une relation se déclare des deux côtés : village et villageId sur Participation (la clé étrangère, avec fields et references), participations sur Village (l’inverse, sans colonne).

Fenêtre de terminal
npx prisma migrate dev --name init
npx prisma generate

migrate dev compare le schéma à la base, écrit la migration dans prisma/migrations/ (un fichier SQL, à relire comme en section d) et l’applique : là où TypeORM demande deux commandes, Prisma n’en demande qu’une. generate fabrique ensuite le client typé, dans src/generated/prisma : migrate dev ne le fait pas à ta place. Ce dossier est produit par la machine, pas par toi : ajoute src/generated au .gitignore, et relance npx prisma generate après chaque changement du schéma (ou après avoir cloné le projet).

Le client généré n’est pas un provider NestJS : il faut l’envelopper dans un service, que tu écris une fois pour tout le projet.

src/prisma/prisma.service.ts
import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PrismaPg } from '@prisma/adapter-pg';
import { PrismaClient } from '../generated/prisma/client.js';
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
constructor(config: ConfigService) {
super({ adapter: new PrismaPg({ connectionString: config.get<string>('DATABASE_URL') }) });
}
async onModuleInit() {
await this.$connect();
}
async onModuleDestroy() {
await this.$disconnect();
}
}

PrismaService est un PrismaClient (extends), configuré avec l’adaptateur. onModuleInit et onModuleDestroy sont des méthodes que NestJS appelle lui-même, l’une quand le module est prêt, l’autre à l’arrêt de l’application : on y ouvre et on y ferme la connexion. Un module @Global() l’exporte, pour n’avoir à l’importer nulle part ailleurs :

src/prisma/prisma.module.ts
import { Global, Module } from '@nestjs/common';
import { PrismaService } from './prisma.service.js';
@Global()
@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export class PrismaModule {}

Ajoute PrismaModule aux imports d’AppModule (avec ConfigModule.forRoot({ isGlobal: true })), et le service des villages s’écrit ainsi :

src/villages/villages.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service.js';
@Injectable()
export class VillagesService {
constructor(private readonly prisma: PrismaService) {}
creer(nom: string) {
return this.prisma.village.create({ data: { nom } });
}
async trouverUn(id: number) {
const village = await this.prisma.village.findUnique({
where: { id },
include: { participations: { include: { utilisateur: true } } },
});
if (!village) throw new NotFoundException(`Village ${id} introuvable`);
return village;
}
}

include joue le rôle de l’option relations de TypeORM : sans lui, les participations ne sont pas chargées. La réponse de GET /api/villages/1 a exactement la forme de celle de la section c.

L’avantage que Prisma met en avant se voit à la compilation. Écris data: { nam: nom } au lieu de data: { nom }, et TypeScript refuse, avant même de lancer le serveur :

error TS2353: Object literal may only specify known properties, and 'nam' does not exist in type …

Le client a été généré à partir du schéma : il sait quelles tables et quelles colonnes existent, et te le rappelle à chaque frappe. Avec TypeORM, create({ nam: … }) est déjà refusé aussi ; la différence est ailleurs — dans d’où vient le type : ici d’un fichier de schéma, là de tes classes.

TypeORM Prisma
Décrire les tables des classes décorées (les entités) un fichier schema.prisma
Les types TypeScript tes classes un client généré depuis le schéma
Intégration NestJS paquet officiel (@nestjs/typeorm, @InjectRepository) à écrire : un PrismaService
Migrations migration:generate, puis migration:run prisma migrate dev, en une commande
Charger les données liées l’option relations l’option include
L’étape à ne pas oublier construire le projet avant la CLI prisma generate après chaque changement de schéma

Il n’y a pas de mauvais choix : les notions de la partie — clés étrangères, migrations, données de test — sont les mêmes avec les deux. Le cours continue avec TypeORM, mieux intégré à NestJS.

Exercices 5.19 à 5.21

Projet : marketplace

Dans un nouveau projet NestJS, avec une base séparée (docker compose exec db psql -U marketplace -d marketplace -c 'CREATE DATABASE marketplace_prisma;'), installe Prisma 7 et écris prisma.config.ts et prisma/schema.prisma avec Vendeur et Produit (nom, prix en Decimal @db.Decimal(10, 2), categorie, et la relation, avec onDelete: Cascade). Lance migrate dev, puis generate, et vérifie avec \dt que les tables existent. Ouvre le fichier SQL créé dans prisma/migrations/ : c’est la même chose que la migration de la section d.

Écris PrismaService et PrismaModule. Ajoute POST /api/vendeurs et GET /api/vendeurs/:id, qui renvoie le vendeur avec ses produits (include). Renvoie une NotFoundException si findUnique ne trouve rien.

Ajoute POST /api/vendeurs/:id/produits, en rattachant le produit avec vendeur: { connect: { id } }. Vérifie que prix revient sous forme de chaîne, comme avec TypeORM. Rattache un produit à un vendeur inexistant : Prisma lève une erreur de code P2025 (Prisma.PrismaClientKnownRequestError, importé depuis le client généré). Attrape-la pour répondre 404. Enfin, fais une faute de frappe sur un nom de champ et note le message de compilation.

Pour vérifier ton travail : npm run test:partie-5 (dépôt d'exercices)