6. ORM & Prisma
Pour le moment, nos requêtes HTTP ne permettent pas de persistence de données. Cela signifie que si nous redémarrons notre serveur, nous perdons toutes les données. Pour éviter cela, nous allons devoir utiliser une base de données.
Intégrer SQL
Voyons comment interagir avec une base de données en utilisant du SQL brut.
Installation d'un client SQL
Pour SQLite, nous pouvons utiliser le package better-sqlite3 :
npm install better-sqlite3
npm install --save-dev @types/better-sqlite3
Connexion et création de table
import Database from 'better-sqlite3'
// Connexion à la base de données
const db = new Database('./database.db')
// Création d'une table utilisateurs
db.exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL
)
`)
Requêtes SQL brutes
Fichier user.route.ts :
import {Router} from 'express'
import Database from 'better-sqlite3'
export const userRouter = Router()
const db = new Database('./database.db')
// GET: Récupérer tous les utilisateurs
// Accessible via GET /users
userRouter.get('/', (_req, res) => {
const users = db.prepare('SELECT * FROM users').all()
res.json(users)
})
// GET: Récupérer un utilisateur par ID
// Accessible via GET /users/:id
userRouter.get('/:id', (req, res) => {
const {id} = req.params
const user = db.prepare('SELECT * FROM users WHERE id = ?').get(id)
if (!user) {
return res.status(404).json({error: 'Utilisateur non trouvé'})
}
res.json(user)
})
// POST: Créer un utilisateur
// Accessible via POST /users
userRouter.post('/', (req, res) => {
const {name, email} = req.body
try {
const result = db
.prepare('INSERT INTO users (name, email) VALUES (?, ?)')
.run(name, email)
res.status(201).json({
message: 'Utilisateur créé',
id: result.lastInsertRowid,
name,
email,
})
} catch (error: any) {
res.status(400).json({error: error.message})
}
})
Les problèmes du SQL brut
Comme vous pouvez le voir, écrire du SQL brut présente plusieurs inconvénients :
- Verbosité : Chaque requête nécessite d'écrire du SQL manuellement
- Pas de typage : TypeScript ne peut pas vérifier si vos requêtes sont correctes
- Gestion d'erreurs complexe : Il faut gérer manuellement chaque cas d'erreur SQL
- Migrations manuelles : Modifier la structure de la base nécessite de réécrire les requêtes
- Risque de sécurité : Même avec les paramètres préparés (
?), il faut être vigilant contre les injections SQL - Code difficile à maintenir : Les requêtes complexes deviennent rapidement illisibles dans les gros projets
- Portabilité limitée : Changer de SGBD (MySQL → PostgreSQL) nécessite de réécrire les requêtes
C'est là qu'interviennent les ORM pour simplifier tout cela et garantir la sécurité et la maintenabilité du code.
ORM
Les ORM (Object-Relational Mapping) sont des outils permettant de gérer les interactions entre une base de données relationnelle et le code d'une application. Ils simplifient les requêtes et la gestion des données en traduisant des objets dans votre code en enregistrements dans la base de données, et vice-versa.
Dans un environnement Node.js, les ORM jouent un rôle crucial en permettant de :
- Simplifier les requêtes SQL complexes
- Gérer les migrations de manière fluide
- Éviter l'écriture de SQL brut, ce qui améliore la maintenabilité du code
- Accéder aux bases de données de manière déclarative et sécurisée
Pourquoi utiliser un ORM ?
L'utilisation d'un ORM présente plusieurs avantages importants :
- Sécurité renforcée : L'ORM réduit les risques d'injections SQL en générant automatiquement des requêtes sécurisées
- Abstraction des bases de données : Vous pouvez interagir avec la base sans connaître en détail le SQL, rendant le code plus lisible et maintenable
- Gestion des migrations : L'ORM facilite les évolutions de la base de données, crucial dans les environnements agiles
- Performance optimisée : Les ORM modernes comme Prisma génèrent des requêtes SQL performantes
- Typage automatique : Avec TypeScript, les ORMs offrent une sécurité de type et de l'autocomplétion
Prisma
Prisma est un ORM (Object-Relational Mapping) moderne qui simplifie la gestion des bases de données dans les projets Node.js. Contrairement aux ORM traditionnels, Prisma adopte une approche déclarative qui facilite la synchronisation entre votre code et votre base de données.
Prisma offre de nombreux avantages :
- Facilité d'utilisation : Écriture simple et intuitive des requêtes grâce au Prisma Client.
- Type safety : Prisma génère automatiquement des types TypeScript basés sur votre schéma, réduisant les erreurs.
- Automatisation des migrations : Gestion des changements dans votre base de données avec un minimum d'effort.
- Performances optimisées : Prisma utilise des requêtes SQL optimisées.
Concepts de base
Prisma repose sur trois composants principaux :
prisma/schema.prisma: Un fichier.prismaqui définit le modèle de données, les relations, et les configurations de la base de données.- Prisma Client : Un client généré automatiquement pour interagir avec la base de données à travers votre code.
- Prisma CLI : Un outil en ligne de commande pour gérer le schéma, les migrations, et bien plus.
Schéma simplifié d'illustration Prisma

Installation et initialisation
Pour ajouter Prisma à votre projet, vous pouvez utiliser :
npm install prisma --save-dev
npm install @prisma/client
Ensuite, initialisez Prisma dans votre projet :
npx prisma init
Cette commande crée deux fichiers principaux :
prisma/schema.prisma: Le fichier principal pour définir votre modèle..env: Un fichier de configuration pour les variables d'environnement, comme l'URL de votre base de données.
Connecteurs
Prisma prend en charge plusieurs connecteurs de base de données, tels que MySQL, PostgreSQL, SQLite, et
SQL Server.
datasource db {
provider = "sqlite"
}
Le fichier de config de prisma prisma.config.ts doit aussi être mis à jour pour utiliser le bon connecteur.
import "dotenv/config";
import {defineConfig, env} from "prisma/config";
export default defineConfig({
schema: "prisma/schema.prisma",
migrations: {
path: "prisma/migrations",
},
datasource: {
url: env("DATABASE_URL"),
},
});
DATABASE_URL="file:./dev.db"
Migration
Une migration Prisma correspond à un ensemble de modifications appliquées à une base de données pour aligner sa
structure (tables, colonnes, relations, etc.) avec le modèle de données défini dans le fichier schema.prisma. Ce
processus est utilisé pour synchroniser le modèle Prisma avec la base de données, particulièrement dans les projets
où
le modèle évolue fréquemment.
- Créer un schéma : Dans le fichier
schema.prismavous pouvez définir un modèle correspondant à une table.
model User {
id Int @id @default(autoincrement())
name String
email String @unique
}
Ce modèle définit une table
usersavec trois colonnes :id(clé primaire auto-incrémentée),name(texte obligatoire) et
-
Créer une migration :
npx prisma migrate dev --name init
Cette commande :
- Génère un fichier de migration contenant les instructions SQL nécessaires.
- Applique les modifications à votre base de données.
Generate
La commande generate sert à recréer le Prisma Client lorsque votre schéma change. Sans cette commande, les
modifications dans schema.prisma ne seront pas reflétées dans votre code.
-
Générer le client :
npx prisma generate -
Quand l'utiliser ? : Après toute modification du fichier
schema.prismaou lors de l'installation de nouvelles dépendances Prisma.
Seed
Le seeding consiste à insérer des données initiales dans votre base, par exemple pour des tests ou un environnement de développement.
- Configurer le script de seed : Dans
prisma.config.ts, ajoutez :
import "dotenv/config";
import {defineConfig, env} from "prisma/config";
export default defineConfig({
schema: "prisma/schema.prisma",
migrations: {
path: "prisma/migrations",
seed: "tsx prisma/seed.ts",
},
datasource: {
url: env("DATABASE_URL"),
},
});
- Créer le script de seed : Exemple de fichier
prisma/seed.ts:
import {PrismaClient} from '../src/generated/prisma/index.js'
import {PrismaBetterSqlite3} from '@prisma/adapter-better-sqlite3'
const adapter = new PrismaBetterSqlite3({
url: process.env.DATABASE_URL || 'file:./dev.db',
})
const prisma = new PrismaClient({adapter})
async function main() {
// Suppression de tous les utilisateurs
await prisma.user.deleteMany()
// Réinitialisation de l'auto-incrémentation (spécifique à SQLite)
await prisma.$executeRaw`DELETE
FROM sqlite_sequence
WHERE name = 'User'`
// Création de plusieurs utilisateurs avec createMany
await prisma.user.createMany({
data: [
{
name: 'Alice',
email: 'alice@example.com',
},
{
name: 'Bob',
email: 'bob@example.com',
},
{
name: 'John Doe',
email: 'john@example.com',
},
],
})
console.log('Base de données peuplée avec succès !')
}
main()
.catch((e) => {
throw e
})
.finally(async () => {
await prisma.$disconnect()
})
- Exécuter le seed :
npx prisma db seed
La commande
executeRawdans ce cas est nécessaire sur SQLite pour réinitialiser l'auto-incrémentation des ID après la suppression des données.
Mise en pratique
Utiliser le client Prisma
Maintenant que nous avons configuré Prisma et peuplé notre base de données, voyons comment utiliser le Prisma Client pour créer les mêmes endpoints que nous avions avec SQL brut, mais de manière plus simple et sécurisée.
Pattern singleton
Pour éviter de créer une nouvelle instance du Prisma Client à chaque requête, il est recommandé d'utiliser un pattern
singleton pour garantir une seule instance partagée entre les requêtes. Créons cela dans un fichier ./src/client.ts :
import {PrismaBetterSqlite3} from '@prisma/adapter-better-sqlite3'
import {PrismaClient} from "@/generated/prisma/client";
const adapter = new PrismaBetterSqlite3({
url: process.env.DATABASE_URL || 'file:./dev.db',
})
const prisma = new PrismaClient({adapter})
export default prisma
Mise à jour de l'api
Reprenons les mêmes endpoints que nous avions créés avec SQL brut, mais cette fois avec Prisma.
Fichier user.route.ts :
import {Router, Request, Response} from 'express'
import {prisma} from './client' // Import du client singleton
export const userRouter = Router()
// GET: Récupérer tous les utilisateurs
// Accessible via GET /users
userRouter.get('/', async (_req: Request, res: Response) => {
const users = await prisma.user.findMany()
res.status(200).json(users)
})
// GET: Récupérer un utilisateur par ID
// Accessible via GET /users/:id
userRouter.get('/:id', async (req: Request, res: Response) => {
const {id} = req.params
const user = await prisma.user.findUnique({
where: {id: parseInt(id)},
})
if (!user) {
return res.status(404).json({error: 'Utilisateur non trouvé'})
}
res.status(200).json(user)
})
// POST: Créer un utilisateur
// Accessible via POST /users
userRouter.post('/', async (req: Request, res: Response) => {
const {name, email} = req.body
try {
const user = await prisma.user.create({
data: {name, email},
})
res.status(201).json({
message: 'Utilisateur créé',
...user,
})
} catch (error: any) {
res.status(400).json({error: error.message})
}
})
Prisma studio
Prisma Studio est un outil de visualisation de données qui vous permet d'explorer et de modifier les données de votre base de données. Pour lancer Prisma Studio, exécutez :
npx prisma studio
Scripts
Pour faciliter votre workflow avec Prisma, vous pouvez ajouter des scripts npm dans votre package.json. Voici quelques
scripts utiles qui vous feront gagner du temps :
{
"scripts": {
"db:migrate": "prisma migrate dev",
"db:generate": "prisma generate",
"db:seed": "prisma db seed",
"db:studio": "prisma studio",
"db:push": "prisma db push",
"db:reset": "prisma migrate reset",
"db:setup": "prisma generate && prisma migrate dev && prisma db seed"
}
}
Description des scripts :
db:migrate: Crée et applique une nouvelle migration en développementdb:generate: Régénère le Prisma Client après modification du schémadb:seed: Exécute le script de seed pour peupler la base de donnéesdb:studio: Lance Prisma Studio pour explorer vos donnéesdb:push: Synchronise le schéma avec la base sans créer de migration (prototypage rapide)db:reset: Réinitialise complètement la base de données et applique toutes les migrationsdb:setup: Script complet pour configurer la base de données (generate + migrate + seed)
Astuce : Le script
db:setupest particulièrement utile pour l'onboarding de nouveaux développeurs, car il configure automatiquement toute la base de données en une seule commande.
Commandes utiles
Prisma Client - Opérations de lecture
findMany: Récupère plusieurs enregistrements avec possibilité de filtrage, tri, et pagination.findUnique: Récupère un enregistrement unique basé sur un identifiant ou champ unique.findFirst: Récupère le premier enregistrement correspondant aux critères.
Prisma Client - Opérations d'écriture
create: Crée un nouvel enregistrement dans la base de données.createMany: Crée plusieurs enregistrements en une seule opération.update: Met à jour un enregistrement existant.updateMany: Met à jour plusieurs enregistrements correspondant aux critères.upsert: Met à jour un enregistrement s'il existe, sinon le crée.delete: Supprime un enregistrement.deleteMany: Supprime plusieurs enregistrements correspondant aux critères.
Prisma CLI - Commandes essentielles
prisma init: Initialise Prisma dans un projet existant.prisma migrate dev: Crée et applique les migrations en développement.prisma migrate deploy: Applique les migrations en production.prisma db seed: Exécute le script de seed pour peupler la base de données.prisma generate: Régénère le Prisma Client après modification du schéma.prisma studio: Lance l'interface graphique Prisma Studio pour explorer et éditer les données.prisma db push: Synchronise le schéma avec la base sans créer de migration (utile en développement rapide).
Les relations
Dans cet exemple, nous avons utilisé un modèle simple avec uniquement des utilisateurs. Cependant, Prisma excelle particulièrement dans la gestion des relations entre les différentes entités de votre base de données.
#$## Exemple de relations
Imaginons que nous voulions ajouter des posts liés aux utilisateurs :
model User {
id Int @id @default(autoincrement())
name String
email String @unique
posts Post[] // Un utilisateur peut avoir plusieurs posts
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
author User @relation(fields: [authorId], references: [id])
authorId Int // Clé étrangère vers User
}
Requêtes avec relations
Avec ce schéma, vous pouvez facilement récupérer un utilisateur avec tous ses posts :
// Récupérer un utilisateur avec tous ses posts
const userWithPosts = await prisma.user.findUnique({
where: {id: 1},
include: {posts: true}, // Inclut les posts liés
})
// Créer un utilisateur avec un post en même temps
const userWithPost = await prisma.user.create({
data: {
name: 'Charlie',
email: 'charlie@example.com',
posts: {
create: [
{
title: 'Mon premier post',
content: 'Contenu du post',
},
],
},
},
include: {posts: true}, // Retourne l'utilisateur avec ses posts
})
Pour en savoir plus : Consultez la documentation officielle sur les différents types de relations :