Changer le thème

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

Easton editorial illustration: cache waterfall instrument

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 : select limite 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 :

  1. Crée prisma/schema.prisma (config centrale)
  2. Crée .env avec DATABASE_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 :

  1. globalForPrisma typise globalThis pour TypeScript
  2. globalForPrisma.prisma || new PrismaClient() : réutiliser si existant, sinon créer
  3. NODE_ENV !== 'production' : cache sur globalThis uniquement 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 : Userusers, Postposts

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 User et authorId Int

@relation(fields: [authorId], references: [id]) :

  • fields: [authorId] : clé étrangère dans Post
  • references: [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, notIn
  • contains, startsWith, endsWith
  • gt, 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, include contre N+1, pool adapté
  • Migrations : migrate dev en dev, migrate deploy en prod
  • Débogage : logs, Studio, codes d’erreur
  • Déploiement : prisma generate au 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. 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. 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. 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. 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. 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. 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 ?
C'est un problème de fuite de connexion causé par le hot reload de Next.js.

À 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 ?
Prisma :
• 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 ?
Ajouter une clé étrangère côté « many », avec @relation.

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 ?
include :
• 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 ?
Utiliser include pour charger les relations en une seule requête, plutôt que dans une boucle. Exemple : prisma.user.findMany({ include: { posts: true } }) récupère tous les utilisateurs et leurs articles en une fois, au lieu d'une requête par utilisateur.
Prisma supporte-t-il les transactions ?
Oui. Utilisez prisma.$transaction([...]) pour exécuter plusieurs opérations : tout réussit ou tout échoue. Exemple : await prisma.$transaction([prisma.user.create(...), prisma.post.create(...)]).
Comment déployer Prisma sur Vercel ?
Étapes :
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

Commentaires

Connectez-vous avec GitHub pour laisser un commentaire

Easton BlogEaston Blog