Guide complet Next.js + Prisma : de la configuration à la pratique (avec solution aux fuites de connexion)

Le terminal affiche encore cette ligne rouge désespérante : Error: Can't reach database server at localhost:5432. Ou plutôt, plus précisément : FATAL: sorry, too many clients already.
La base tourne, la chaîne de connexion est correcte — pourquoi ça ne se connecte pas ? Au démarrage du projet, tout allait bien ; après quelques modifications de code, les erreurs commencent. Redémarrer le serveur de dev Next.js ? Inutile. Redémarrer la base ? Ça repart un moment, puis ça replante.
On apprend ensuite que c’est l’un des pièges les plus classiques en dev Next.js + Prisma : les fuites de connexion au hot reload. En bref, à chaque modification, Next.js recrée une instance Prisma, mais les anciennes connexions ne se ferment pas — le pool finit par saturer.
Vous avez peut-être vécu la même chose : ajouter une base à un projet Next.js, hésiter entre Prisma, TypeORM et Drizzle ; choisir Prisma, configurer, et tomber sur des erreurs ; ne pas savoir comment modéliser one-to-many ou many-to-many ; ou, comme moi, voir la base lâcher en plein développement.
En réalité, la courbe d’apprentissage de Prisma n’est pas si raide, et la configuration reste simple. L’essentiel : savoir configurer l’environnement, éviter les fuites de connexion, concevoir le Schema et écrire le CRUD. Cet article vise à clarifier tout ça et vous faire gagner du temps.
Pourquoi choisir Prisma ? (comparaison avec TypeORM et SQL natif)
Pour la base de données dans Next.js, les options ne manquent pas. TypeORM est un vieux de la maison, Drizzle est léger et moderne, le SQL natif offre des performances maximales. Alors pourquoi Prisma ?
Typage sûr, vraiment sûr
La première fois que j’ai vu les types TypeScript générés automatiquement par Prisma, j’ai été bluffé. Pas un simple « pas mal », plutôt un « wow, on peut faire ça ».
Vous écrivez le Schema, lancez npx prisma generate, et Prisma génère les définitions de types pour tous les modèles. Pas de simples interfaces — des types complets avec autocomplétion. Quand vous tapez prisma.user.findUnique({ where: { id: , VSCode vous indique le type de id et les champs disponibles dans where.
Comparaison :
- TypeORM : décorateurs
@Entity()@Column()à la main, types séparés du schema — facile de désynchroniser - SQL natif : résultats en
any, interfaces manuelles, mise à jour à chaque changement de table - Prisma : le Schema est la source de vérité, types synchronisés automatiquement, regénérer après modification
Ce n’est pas qu’une question de confort : le typage sûr détecte les erreurs à l’écriture, pas au runtime.
Expérience de développement, les détails comptent
Quelques points qui me plaisent particulièrement :
Prisma Studio : interface visuelle pour gérer la base. npx prisma studio ouvre le navigateur pour consulter et éditer les données. Plus besoin de TablePlus ou de requêtes SQL ad hoc en dev.
Migrations : prisma migrate dev génère et applique les migrations — bien plus simple que TypeORM. Prisma détecte les changements du Schema et propose de générer une migration. Pas de scripts SQL manuels ni de souci d’ordre.
Syntaxe de requête : très naturelle pour un développeur JavaScript :
const users = await prisma.user.findMany({
where: { email: { contains: '@gmail.com' } },
include: { posts: true },
orderBy: { createdAt: 'desc' }
})
Intuitif : pas de SQL, pas de décorateurs — des objets et des appels de méthodes.
Comparé au QueryBuilder TypeORM :
const users = await userRepository.createQueryBuilder("user")
.where("user.email LIKE :email", { email: "%@gmail.com%" })
.leftJoinAndSelect("user.posts", "posts")
.orderBy("user.createdAt", "DESC")
.getMany()
Même fonctionnalité, mais Prisma est plus lisible et moins sujet aux erreurs.
Performance et écosystème
On entend parfois : « Les ORM, c’est lent — en prod, il faut du SQL natif. »
C’est un cliché. Prisma a un surcoût, mais pour la plupart des projets, c’est acceptable. Et Prisma optimise pas mal :
- N+1 : avec
include, Prisma fusionne les requêtes - Pool de connexions : config par défaut raisonnable,
num_cpus * 2 + 1 - Requêtes :
selectlimite les champs transférés
Goulot d’étranglement réel ? Prisma accepte aussi le SQL natif :
const result = await prisma.$queryRaw`SELECT * FROM User WHERE id = ${userId}`
Confort de l’ORM et SQL natif là où il faut.
Côté écosystème : 38k+ stars GitHub, recommandation Vercel, documentation complète. La doc Next.js officielle inclut un guide d’intégration Prisma — c’est clairement un choix mainstream.
Prisma n’est pas parfait, mais pour la plupart des projets full-stack Next.js, type safety, DX et écosystème suffisent largement.
Configuration de l’environnement : Prisma from scratch
Prisma choisi, passons à la config. Dix minutes suffisent en général.
Installation et initialisation
Supposons un projet Next.js existant ; sinon npx create-next-app@latest.
Installer Prisma :
npm install prisma @prisma/client
Deux paquets :
prisma: CLI (init, migrations, Studio)@prisma/client: client de requêtes
Puis initialiser :
npx prisma init
Cette commande :
- Crée
prisma/schema.prisma(config centrale) - Crée
.envavecDATABASE_URL
Configurer la connexion à la base
Dans .env :
DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"
Exemple PostgreSQL. Format :
postgresql://utilisateur:motdepasse@hôte:port/nom_base?schema=public
En local, PostgreSQL est recommandé : fonctionnalités complètes, excellent support Prisma, souvent le même en production.
Pas de PostgreSQL ? Docker est le plus rapide :
Créer docker-compose.yml :
version: '3.8'
services:
postgres:
image: postgres:15
restart: always
environment:
POSTGRES_USER: myuser
POSTGRES_PASSWORD: mypassword
POSTGRES_DB: mydb
ports:
- '5432:5432'
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
Lancer :
docker-compose up -d
Puis ajuster .env :
DATABASE_URL="postgresql://myuser:mypassword@localhost:5432/mydb?schema=public"
Important : ne commitez pas .env ! Vérifiez .gitignore :
.env
.env.local
Sinon les mots de passe fuient.
MySQL ou SQLite — format légèrement différent :
MySQL :
DATABASE_URL="mysql://root:password@localhost:3306/mydb"
SQLite (fichier local, petits projets) :
DATABASE_URL="file:./dev.db"
Générer le Prisma Client
Après la connexion :
npx prisma generate
Lit schema.prisma, génère types et méthodes dans node_modules/@prisma/client.
À chaque modification du Schema, relancer npx prisma generate — sinon TypeScript signale des incohérences de types.
Environnement prêt : install → init → connexion → generate. Rien de compliqué.
Prochaine étape : les fuites de connexion au hot reload — le point où beaucoup se font piéger.
Résoudre les fuites de connexion au hot reload (chapitre clé)
Voici le problème qui fait pleurer : FATAL: sorry, too many clients already.
Qu’est-ce qui se passe ?
En dev, Next.js utilise le Hot Module Replacement (HMR) : vous sauvegardez, la page se rafraîchit sans redémarrer le serveur. Très pratique.
Avec Prisma, ça coince.
À chaque modification, Next.js recharge les modules. Si vous faites new PrismaClient() directement dans une route API ou un Server Component, chaque hot reload crée une nouvelle instance.
Chaque instance ouvre de nouvelles connexions. Les anciennes restent ouvertes.
PostgreSQL accepte par défaut ~100 connexions. Quelques sauvegardes : 10, 20, 50, 100 — saturé. La base refuse les nouvelles connexions : too many clients already.
La première fois, j’étais perdu. Base OK, pourquoi ça casse ? Redémarrer le serveur ? Ça repart puis replante. L’Issue #10247 sur GitHub regorge de témoignages identiques.
Pattern singleton : une solution simple
Un seul PrismaClient pour toute l’application, via un singleton.
Stocker l’instance sur globalThis — objet global JavaScript non vidé au hot reload. Première création, puis réutilisation à chaque reload.
Créer lib/prisma.ts à la racine :
import { PrismaClient } from '@prisma/client'
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined
}
export const prisma = globalForPrisma.prisma || new PrismaClient({
log: ['query', 'error', 'warn'], // voir toutes les requêtes en dev
})
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma
}
Explication :
globalForPrismatypiseglobalThispour TypeScriptglobalForPrisma.prisma || new PrismaClient(): réutiliser si existant, sinon créerNODE_ENV !== 'production': cache surglobalThisuniquement en dev — en prod pas de hot reload
Importer prisma partout depuis ce fichier :
App Router (Next.js 13+) — Server Component :
// app/users/page.tsx
import { prisma } from '@/lib/prisma'
export default async function UsersPage() {
const users = await prisma.user.findMany()
return (
<div>
{users.map(user => (
<div key={user.id}>{user.name}</div>
))}
</div>
)
}
Route API :
// app/api/users/route.ts
import { NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
export async function GET() {
const users = await prisma.user.findMany()
return NextResponse.json(users)
}
Pages Router (Next.js 12 et avant) :
// pages/api/users.ts
import type { NextApiRequest, NextApiResponse } from 'next'
import { prisma } from '@/lib/prisma'
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const users = await prisma.user.findMany()
res.status(200).json(users)
}
Règle d’or : jamais new PrismaClient() dans un fichier — toujours importer depuis lib/prisma.ts.
Pas de souci en production
En prod (Vercel, etc.), pas de hot reload. Code buildé, peu de rechargements.
NODE_ENV !== 'production' : singleton en dev, création normale en prod.
Solution recommandée par Prisma dans la doc officielle. Une fois lib/prisma.ts en place, les fuites disparaissent.
Bonnes pratiques de conception du Schema
Environnement OK, fuites résolues — modélisons les tables. Tout est dans prisma/schema.prisma.
Définition de modèles de base
Exemple simple — blog avec User et Post :
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
password String
posts Post[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
authorId Int
author User @relation(fields: [authorId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
Points clés :
@id: clé primaire@default(autoincrement()): ID auto-incrémenté@unique: contrainte d’unicité (email)String?: champ optionnel (nullable)@default(now()): horodatage à la création@updatedAt: mise à jour automatique
Conventions :
- Modèles en PascalCase :
User,Post,UserProfile - Champs en camelCase :
createdAt,authorId - Tables en minuscules pluriel :
User→users,Post→posts
Relations : one-to-many, many-to-many, one-to-one
Le cœur du Schema — souvent source de confusion.
One-to-many
Un utilisateur, plusieurs articles ; un article, un seul auteur.
Dans l’exemple ci-dessus :
User:posts Post[]Post:author UseretauthorId Int
@relation(fields: [authorId], references: [id]) :
fields: [authorId]: clé étrangère dans Postreferences: [id]: clé référencée dans User
Many-to-many
Plusieurs tags par article, plusieurs articles par tag.
Deux approches : implicite ou table intermédiaire explicite.
Implicite (simple, géré par Prisma) :
model Post {
id Int @id @default(autoincrement())
title String
tags Tag[]
}
model Tag {
id Int @id @default(autoincrement())
name String
posts Post[]
}
Prisma crée _PostToTag automatiquement. Requête : include: { tags: true }.
Explicite (champs supplémentaires sur la jointure) :
model Post {
id Int @id @default(autoincrement())
title String
postTags PostTag[]
}
model Tag {
id Int @id @default(autoincrement())
name String
postTags PostTag[]
}
model PostTag {
id Int @id @default(autoincrement())
postId Int
tagId Int
post Post @relation(fields: [postId], references: [id])
tag Tag @relation(fields: [tagId], references: [id])
createdAt DateTime @default(now())
@@unique([postId, tagId]) // éviter les doublons
}
Utile pour createdAt sur la jointure. Sans champ extra, l’implicite suffit.
One-to-one
Un utilisateur, un profil ; un profil, un utilisateur.
model User {
id Int @id @default(autoincrement())
email String @unique
profile Profile?
}
model Profile {
id Int @id @default(autoincrement())
bio String?
userId Int @unique
user User @relation(fields: [userId], references: [id])
}
userId avec @unique : un seul Profile par User.
Techniques avancées
Enum :
enum Role {
USER
ADMIN
MODERATOR
}
model User {
id Int @id @default(autoincrement())
email String @unique
role Role @default(USER)
}
Index unique composite :
model Post {
id Int @id @default(autoincrement())
title String
authorId Int
slug String
@@unique([authorId, slug]) // slug unique par auteur
}
Relations multiples — paramètre name :
model User {
id Int @id @default(autoincrement())
writtenPosts Post[] @relation("PostAuthor")
favoritePosts Post[] @relation("PostFavorites")
}
model Post {
id Int @id @default(autoincrement())
title String
authorId Int
author User @relation("PostAuthor", fields: [authorId], references: [id])
favoritedBy User[] @relation("PostFavorites")
}
Sans name, Prisma ne sait pas quelle relation correspond à quoi.
Exemple complet : système de blog
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
enum Role {
USER
ADMIN
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
password String
role Role @default(USER)
posts Post[]
comments Comment[]
profile Profile?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Profile {
id Int @id @default(autoincrement())
bio String?
avatar String?
userId Int @unique
user User @relation(fields: [userId], references: [id])
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
authorId Int
author User @relation(fields: [authorId], references: [id])
comments Comment[]
tags Tag[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Comment {
id Int @id @default(autoincrement())
content String
postId Int
post Post @relation(fields: [postId], references: [id])
authorId Int
author User @relation(fields: [authorId], references: [id])
createdAt DateTime @default(now())
}
model Tag {
id Int @id @default(autoincrement())
name String @unique
posts Post[]
}
Puis npx prisma migrate dev --name init — tables, index et contraintes créés automatiquement.
Comprendre one-to-many, many-to-many et one-to-one, plus enums et index uniques, couvre la plupart des cas.
CRUD en pratique
Schema prêt — manipulons les données. L’API Prisma est intuitive.
Toujours importer prisma depuis lib/prisma.ts, jamais new PrismaClient().
Créer (Create)
Un enregistrement :
import { prisma } from '@/lib/prisma'
const user = await prisma.user.create({
data: {
email: '[email protected]',
name: 'Alice',
password: 'hashed_password_here'
}
})
En lot :
const users = await prisma.user.createMany({
data: [
{ email: '[email protected]', name: 'Bob', password: 'pass1' },
{ email: '[email protected]', name: 'Charlie', password: 'pass2' }
],
skipDuplicates: true
})
console.log(`${users.count} utilisateurs créés`)
Création imbriquée (User + Profile) :
const user = await prisma.user.create({
data: {
email: '[email protected]',
name: 'Dave',
password: 'pass',
profile: {
create: {
bio: 'Développeur passionné de programmation'
}
}
},
include: {
profile: true
}
})
Lire (Read)
Un enregistrement :
const user = await prisma.user.findUnique({
where: { email: '[email protected]' }
})
const firstPost = await prisma.post.findFirst({
where: { published: true },
orderBy: { createdAt: 'desc' }
})
Plusieurs enregistrements :
const users = await prisma.user.findMany({
where: {
email: {
contains: '@gmail.com'
}
},
orderBy: { createdAt: 'desc' },
take: 10,
skip: 0
})
Requêtes relationnelles :
Avec include :
const user = await prisma.user.findUnique({
where: { id: 1 },
include: {
posts: true,
profile: true
}
})
Avec select (moins de données) :
const user = await prisma.user.findUnique({
where: { id: 1 },
select: {
id: true,
email: true,
posts: {
select: {
id: true,
title: true
}
}
}
})
include charge tous les champs ; select limite — préférez select sur gros volumes.
Filtres :
const posts = await prisma.post.findMany({
where: {
OR: [
{ title: { contains: 'Next.js' } },
{ content: { contains: 'Prisma' } }
],
AND: [
{ published: true },
{ authorId: { not: 1 } }
]
}
})
Opérateurs courants :
equals,not,in,notIncontains,startsWith,endsWithgt,gte,lt,lte
Mettre à jour (Update)
Un enregistrement :
const user = await prisma.user.update({
where: { id: 1 },
data: { name: 'Alice Updated' }
})
Plusieurs :
const result = await prisma.user.updateMany({
where: { email: { contains: '@gmail.com' } },
data: { role: 'ADMIN' }
})
console.log(`Mis à jour : ${result.count} utilisateurs`)
Upsert (update ou create) :
const user = await prisma.user.upsert({
where: { email: '[email protected]' },
update: { name: 'Alice Updated' },
create: {
email: '[email protected]',
name: 'Alice',
password: 'pass'
}
})
Pratique — pas besoin de find puis branchement create/update.
Supprimer (Delete)
Un enregistrement :
const user = await prisma.user.delete({
where: { id: 1 }
})
Plusieurs :
const result = await prisma.user.deleteMany({
where: {
createdAt: {
lt: new Date('2023-01-01')
}
}
})
console.log(`Supprimés : ${result.count} utilisateurs`)
Transactions
Plusieurs opérations atomiques — ex. virement : débit A, crédit B, les deux ou rien.
Transaction par lot :
const [user, post] = await prisma.$transaction([
prisma.user.create({ data: { email: '[email protected]', password: 'pass' } }),
prisma.post.create({ data: { title: 'Test Post', authorId: 1 } })
])
Transaction interactive (dépendances entre étapes) :
const transferMoney = await prisma.$transaction(async (tx) => {
const accountA = await tx.account.update({
where: { id: 1 },
data: { balance: { decrement: 100 } }
})
if (accountA.balance < 0) {
throw new Error('Solde insuffisant')
}
const accountB = await tx.account.update({
where: { id: 2 },
data: { balance: { increment: 100 } }
})
return { accountA, accountB }
})
Erreur en cours de route → rollback complet.
Exemple complet : route API Next.js
// app/api/posts/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const page = parseInt(searchParams.get('page') || '1')
const limit = parseInt(searchParams.get('limit') || '10')
const posts = await prisma.post.findMany({
where: { published: true },
include: {
author: {
select: { id: true, name: true, email: true }
},
tags: true
},
orderBy: { createdAt: 'desc' },
skip: (page - 1) * limit,
take: limit
})
const total = await prisma.post.count({ where: { published: true } })
return NextResponse.json({ posts, total, page, limit })
} catch (error) {
return NextResponse.json({ error: 'Failed to fetch posts' }, { status: 500 })
}
}
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { title, content, authorId, tagIds } = body
const post = await prisma.post.create({
data: {
title,
content,
authorId,
tags: {
connect: tagIds.map((id: number) => ({ id }))
}
},
include: { tags: true }
})
return NextResponse.json(post, { status: 201 })
} catch (error) {
return NextResponse.json({ error: 'Failed to create post' }, { status: 500 })
}
}
// app/api/posts/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const post = await prisma.post.findUnique({
where: { id: parseInt(params.id) },
include: {
author: { select: { id: true, name: true } },
tags: true,
comments: {
include: {
author: { select: { id: true, name: true } }
}
}
}
})
if (!post) {
return NextResponse.json({ error: 'Post not found' }, { status: 404 })
}
return NextResponse.json(post)
} catch (error) {
return NextResponse.json({ error: 'Failed to fetch post' }, { status: 500 })
}
}
export async function PATCH(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const body = await request.json()
const post = await prisma.post.update({
where: { id: parseInt(params.id) },
data: body
})
return NextResponse.json(post)
} catch (error) {
return NextResponse.json({ error: 'Failed to update post' }, { status: 500 })
}
}
export async function DELETE(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
await prisma.post.delete({
where: { id: parseInt(params.id) }
})
return NextResponse.json({ message: 'Post deleted' })
} catch (error) {
return NextResponse.json({ error: 'Failed to delete post' }, { status: 500 })
}
}
Avec create, findUnique, findMany, update, delete, filtres, jointures et transactions, vous couvrez la majorité des besoins.
Techniques avancées et questions fréquentes
CRUD maîtrisé — voici performance, migrations et débogage.
Optimisation des performances
Réduire les champs avec select
Par défaut, Prisma charge tout. Sur grosses tables ou champs texte volumineux :
// Moins bien : tous les champs, y compris content volumineux
const posts = await prisma.post.findMany()
// Mieux : champs nécessaires uniquement
const posts = await prisma.post.findMany({
select: {
id: true,
title: true,
createdAt: true,
author: {
select: { name: true }
}
}
})
En liste, pas besoin du contenu complet.
Éviter N+1
Classique ORM : N enregistrements + N requêtes relationnelles = N+1.
// N+1
const users = await prisma.user.findMany()
for (const user of users) {
user.posts = await prisma.post.findMany({ where: { authorId: user.id } })
}
// Une requête
const users = await prisma.user.findMany({
include: { posts: true }
})
Prisma fusionne via JOIN ou requêtes batch.
Pool de connexions
Taille par défaut : num_cpus * 2 + 1. En serverless (Vercel) ou forte concurrence, ajuster via DATABASE_URL :
DATABASE_URL="postgresql://user:password@localhost:5432/mydb?connection_limit=5"
Serverless : commencer à 1 et monter progressivement.
Sur Vercel, Prisma Accelerate offre pool HTTP et cache global pour le serverless.
Gestion des migrations
Développement
Après modification du Schema :
npx prisma migrate dev --name add-user-role
Détecte les changements, génère le SQL, applique, regénère le Client.
--name descriptif : add-user-role, create-post-table.
Production
Jamais migrate dev en prod — risque de perte de données. Utiliser :
npx prisma migrate deploy
Applique les migrations existantes sans en créer. Dans le CI/CD, avant le déploiement.
Rollback
Pas de commande native. Voir l’historique :
npx prisma migrate status
Rollback manuel (SQL inverse ou snapshot). Sauvegardes régulières en prod.
Débogage
Logs de requêtes :
export const prisma = new PrismaClient({
log: ['query', 'info', 'warn', 'error']
})
En prod, seulement error :
log: ['error']
Prisma Studio :
npx prisma studio
Consulter, éditer, tester les relations — sans client SQL externe.
Erreurs courantes
P2002: Unique constraint failed → contrainte d’unicité violée (email dupliqué).
P2025: Record not found → enregistrement absent pour update/delete.
P1001: Can't reach database server → vérifier DATABASE_URL et que la base tourne.
Déploiement Vercel
Dans package.json :
{
"scripts": {
"build": "prisma generate && next build"
}
}
DATABASE_URL dans les variables Vercel, pas dans le code.
PostgreSQL : Vercel Postgres ou Supabase — quotas gratuits, config simple.
En résumé
- Performance :
select,includecontre N+1, pool adapté - Migrations :
migrate deven dev,migrate deployen prod - Débogage : logs, Studio, codes d’erreur
- Déploiement :
prisma generateau build, variables d’environnement
Conclusion
De la configuration au CRUD, des fuites de connexion au Schema — vous avez parcouru Next.js + Prisma de bout en bout.
Points essentiels :
Fuites de connexion : lib/prisma.ts en singleton, import partout. Indispensable en dev.
Schema : one-to-many, many-to-many, one-to-one ; @relation ; enums et conventions de nommage.
CRUD : create, findMany, update, delete ; include vs select ; $transaction pour la cohérence.
Performance et déploiement : select pour alléger, include contre N+1 ; bonne commande de migration ; prisma generate sur Vercel.
Prisma reste plus accessible que le SQL pur, avec un typage et une DX solides. Pas parfait — surcoût perf, requêtes complexes parfois en SQL natif — mais pour la plupart des projets Next.js full-stack, les avantages l’emportent.
Prochaines étapes :
- Monter un petit projet Next.js + Prisma
- Explorer la documentation Prisma
- Tester Prisma Studio
- Parcourir les GitHub Discussions Prisma — communauté active
Partagez vos galères Prisma en commentaire — votre expérience peut aider d’autres développeurs.
Configuration complète Next.js + Prisma
Étapes complètes de l'installation à la résolution des fuites de connexion, conception du Schema et opérations CRUD
⏱️ Estimated time: 4 hr
- 1
Step 1: Installation et initialisation de Prisma
Installer les dépendances :
• npm install prisma @prisma/client
• npx prisma init
L'initialisation crée :
• prisma/schema.prisma : fichier de définition du Schema
• .env : variables d'environnement (contient DATABASE_URL)
Configurer la connexion :
• Définir DATABASE_URL dans .env
• Format : postgresql://user:password@localhost:5432/dbname - 2
Step 2: Résoudre les fuites de connexion
Créer un singleton :
• Créer le fichier lib/prisma.ts
• En dev, mettre en cache l'instance avec globalThis
• En prod, exporter l'instance directement
Code :
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient }
export const prisma = globalForPrisma.prisma || new PrismaClient()
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
Cela évite la création de multiples connexions au hot reload - 3
Step 3: Concevoir le Schema
Définir les modèles :
• Utiliser le mot-clé model pour les tables
• @id pour la clé primaire
• @default pour les valeurs par défaut
• @relation pour les associations
Types de relations :
• One-to-one : @relation(fields, references)
• One-to-many : un modèle a @relation, l'autre non
• Many-to-many : table intermédiaire (@relation)
Générer la migration :
• npx prisma migrate dev --name init - 4
Step 4: Implémenter les opérations CRUD
Créer des données :
• prisma.user.create({ data: { name, email } })
Lire des données :
• prisma.user.findMany() : plusieurs enregistrements
• prisma.user.findUnique({ where: { id } }) : un seul
• prisma.user.findFirst({ where: { ... } }) : le premier
Mettre à jour :
• prisma.user.update({ where: { id }, data: { name } })
Supprimer :
• prisma.user.delete({ where: { id } }) - 5
Step 5: Gérer les requêtes relationnelles
Avec include :
• prisma.user.findMany({ include: { posts: true } })
• Retourne l'utilisateur et tous ses articles
Avec select :
• prisma.user.findMany({ select: { id: true, name: true } })
• Ne retourne que les champs spécifiés, moins de données
Éviter le problème N+1 :
• Utiliser include pour tout charger en une fois
• Ne pas requêter les relations dans une boucle externe - 6
Step 6: Déploiement et migrations
Déploiement en production :
• Définir la variable DATABASE_URL
• Exécuter npx prisma generate
• Exécuter npx prisma migrate deploy pour appliquer les migrations
Déploiement Vercel :
• Configurer les variables dans le Dashboard Vercel
• Ajouter prisma generate à la commande build
• Utiliser prisma migrate deploy (pas migrate dev)
Attention : ne jamais exécuter migrate dev en production
FAQ
Pourquoi l'erreur 'too many clients already' apparaît-elle ?
À chaque modification de code, Next.js crée une nouvelle instance PrismaClient, mais les anciennes connexions ne se ferment pas automatiquement — le pool finit par être épuisé.
La solution est le pattern singleton : mettre en cache l'instance PrismaClient avec globalThis en développement.
Quelle différence entre Prisma, TypeORM et Drizzle ?
• Meilleur typage et meilleure DX
• Mais un certain surcoût de performance
TypeORM :
• Puissant, requêtes complexes
• Mais configuration lourde
Drizzle :
• Léger, bonnes performances
• Mais typage moins solide que Prisma
Pour un projet Next.js, la simplicité et le typage sûr de Prisma sont un avantage net.
Comment concevoir une relation one-to-many ?
Exemple : un User a plusieurs Post
• Dans Post : champ userId et @relation(fields: ['userId'], references: [id])
• Dans User : champ posts Post[]
Quelle différence entre include et select ?
• Pour les requêtes relationnelles, retourne les données associées
• Augmente le volume de données
select :
• Pour choisir les champs, ne retourne que ceux spécifiés
• Réduit le volume de données
Utilisables ensemble : { include: { posts: true }, select: { id: true, name: true } }
Comment éviter le problème N+1 ?
Prisma supporte-t-il les transactions ?
Comment déployer Prisma sur Vercel ?
1) Configurer DATABASE_URL dans le Dashboard Vercel
2) Ajouter prisma generate à la commande build dans package.json
3) Utiliser prisma migrate deploy (pas migrate dev)
Vérifier que la connexion à la base de production fonctionne.
16 min de lecture · Publié le: 20 déc. 2025 · Mis à jour le: 27 juil. 2026
Guide complet Next.js
Si vous arrivez depuis la recherche, le plus rapide est de passer à l’article précédent ou suivant de cette série.
Précédent
Guide de choix de base de données Next.js : PostgreSQL, MySQL, MongoDB et services cloud comparés
Quelle base de données choisir pour un projet Next.js ? Ce guide compare PostgreSQL, MySQL, MongoDB, analyse Vercel Postgres, Supabase, PlanetScale et MongoDB Atlas, et vous aide à décider en 3 questions.
Partie 22 sur 51
Suivant
Guide de choix de gestion d'état Next.js : Zustand vs Jotai en pratique
Redux trop lourd, Context trop lent ? Comparaison de Zustand et Jotai dans Next.js, guide de choix clair et bonnes pratiques App Router pour choisir une solution légère.
Partie 24 sur 51



Commentaires
Connectez-vous avec GitHub pour laisser un commentaire