Construire une application full-stack avec Prisma ORM et Next.js 15 App Router

Le ORM TypeScript le plus populaire rencontre la stack React moderne. Prisma vous offre un schéma déclaratif, des types auto-générés et un moteur de requêtes puissant — le tout intégré parfaitement avec Next.js 15 Server Components et Server Actions. Dans ce tutoriel, vous construirez une application complète de gestion de projets à partir de zéro.
Ce que vous apprendrez
À la fin de ce tutoriel, vous serez capable de :
- Configurer Prisma ORM avec PostgreSQL dans un projet Next.js 15 App Router
- Définir des modèles et des relations avec Prisma Schema Language
- Exécuter des migrations et seeder la base de données
- Construire des opérations CRUD complètes avec les Server Actions de Next.js
- Utiliser le Prisma Client pour des requêtes typées, des filtres et la pagination
- Gérer la validation des formulaires avec Zod et
useActionState - Déployer en production avec les meilleures pratiques
Prérequis
Avant de commencer, assurez-vous de disposer de :
- Node.js 20+ installé (
node --version) - Expérience en TypeScript (types, interfaces, async/await)
- Familiarité avec Next.js (App Router, Server Components)
- PostgreSQL en local ou une instance cloud (nous utiliserons Neon)
- Un éditeur de code — VS Code ou Cursor recommandé
Pourquoi Prisma ORM ?
Prisma est le ORM TypeScript le plus adopté, utilisé par des entreprises comme Netflix, Notion et Hashicorp. Voici ce qui le distingue :
| Fonctionnalité | Prisma | Drizzle | TypeORM |
|---|---|---|---|
| Schéma | DSL déclaratif (.prisma) | TypeScript | Décorateurs |
| Typage | Types auto-générés | Types inférés | Partiel |
| Migrations | CLI intégré | drizzle-kit | CLI ou manuel |
| Relations | Première classe, écritures imbriquées | Jointures manuelles | Décorateurs |
| API de requête | API objet intuitive | API SQL-like | Pattern Repository |
| Studio | GUI intégré (Prisma Studio) | Drizzle Studio | Aucun |
| Support Edge | Prisma Accelerate | Natif | Non optimisé |
Prisma adopte une approche schema-first : vous déclarez votre modèle de données dans un fichier .prisma, et Prisma génère un client entièrement typé avec autocomplétion pour chaque champ, relation et filtre. Aucune définition de type manuelle nécessaire.
Ce que vous construirez
Une application de gestion de projets avec :
- Des projets contenant plusieurs tâches
- Des tâches avec statut, priorité et dates limites
- CRUD complet pour les projets et les tâches
- Filtrage et tri
- Interface responsive avec Tailwind CSS
Étape 1 : Créer un projet Next.js 15
Créez un nouveau projet :
npx create-next-app@latest project-manager --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"
cd project-managerAcceptez les valeurs par défaut. Cela crée un projet Next.js 15 avec App Router, TypeScript et Tailwind CSS.
Étape 2 : Installer Prisma
Installez Prisma en dépendance de développement et le Prisma Client :
npm install prisma --save-dev
npm install @prisma/clientInitialisez Prisma avec PostgreSQL comme fournisseur :
npx prisma init --datasource-provider postgresqlCela crée deux fichiers :
prisma/schema.prisma— la définition de votre modèle de données.env— la chaîne de connexion à la base de données
Étape 3 : Configurer la base de données
Ouvrez .env et définissez votre chaîne de connexion PostgreSQL :
DATABASE_URL="postgresql://user:password@localhost:5432/project_manager?schema=public"Si vous utilisez Neon (recommandé pour une configuration rapide) :
- Créez un compte gratuit sur neon.tech
- Créez un nouveau projet
- Copiez la chaîne de connexion depuis le tableau de bord
- Collez-la dans
.env
Si vous exécutez PostgreSQL localement avec Docker :
docker run --name pg-prisma -e POSTGRES_USER=user -e POSTGRES_PASSWORD=password -e POSTGRES_DB=project_manager -p 5432:5432 -d postgres:16Étape 4 : Définir votre schéma Prisma
Ouvrez prisma/schema.prisma et définissez le modèle de données :
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Project {
id String @id @default(cuid())
name String
description String?
color String @default("#3b82f6")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
tasks Task[]
@@map("projects")
}
model Task {
id String @id @default(cuid())
title String
description String?
status TaskStatus @default(TODO)
priority Priority @default(MEDIUM)
dueDate DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
projectId String
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
@@index([projectId])
@@index([status])
@@map("tasks")
}
enum TaskStatus {
TODO
IN_PROGRESS
DONE
}
enum Priority {
LOW
MEDIUM
HIGH
URGENT
}Points clés de ce schéma :
@id @default(cuid())génère automatiquement des identifiants uniques@updatedAtmet à jour automatiquement le timestamp à chaque modificationTask[]sur Project définit une relation un-à-plusieursonDelete: Cascadesupprime toutes les tâches quand un projet est supprimé@@indexcrée des index de base de données pour des requêtes plus rapides@@mapmappe le modèle à un nom de table spécifique- Enums fournissent des valeurs de statut et de priorité typées
Étape 5 : Exécuter votre première migration
Générez et appliquez la migration :
npx prisma migrate dev --name initCette commande fait trois choses :
- Crée un fichier de migration SQL dans
prisma/migrations/ - Applique la migration à votre base de données
- Génère le Prisma Client avec des types TypeScript complets
Vous pouvez inspecter le SQL généré dans prisma/migrations/[timestamp]_init/migration.sql.
Étape 6 : Créer le Singleton Prisma Client
Dans un environnement Next.js, le rechargement à chaud peut créer plusieurs instances de Prisma Client. Créez un singleton pour éviter cela.
Créez src/lib/prisma.ts :
import { PrismaClient } from "@prisma/client";
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
export const prisma = globalForPrisma.prisma ?? new PrismaClient();
if (process.env.NODE_ENV !== "production") {
globalForPrisma.prisma = prisma;
}Cela garantit qu'une seule instance de PrismaClient existe pendant le développement.
Étape 7 : Seeder la base de données
Créez prisma/seed.ts pour remplir la base de données avec des données exemples :
import { PrismaClient, TaskStatus, Priority } from "@prisma/client";
const prisma = new PrismaClient();
async function main() {
// Nettoyer les données existantes
await prisma.task.deleteMany();
await prisma.project.deleteMany();
// Créer des projets avec des tâches
const webApp = await prisma.project.create({
data: {
name: "Refonte application web",
description: "Refonte complète du site web de la société",
color: "#3b82f6",
tasks: {
create: [
{
title: "Designer la nouvelle page d'accueil",
description: "Créer les wireframes et maquettes pour la homepage",
status: TaskStatus.DONE,
priority: Priority.HIGH,
dueDate: new Date("2026-04-15"),
},
{
title: "Implémenter l'authentification",
description: "Configurer login, inscription et réinitialisation du mot de passe",
status: TaskStatus.IN_PROGRESS,
priority: Priority.URGENT,
dueDate: new Date("2026-04-20"),
},
{
title: "Construire le tableau de bord",
description: "Créer le dashboard utilisateur principal avec des graphiques",
status: TaskStatus.TODO,
priority: Priority.MEDIUM,
dueDate: new Date("2026-05-01"),
},
{
title: "Rédiger la documentation API",
status: TaskStatus.TODO,
priority: Priority.LOW,
},
],
},
},
});
const mobileApp = await prisma.project.create({
data: {
name: "MVP Application Mobile",
description: "Application React Native pour iOS et Android",
color: "#10b981",
tasks: {
create: [
{
title: "Configurer le projet Expo",
status: TaskStatus.DONE,
priority: Priority.HIGH,
},
{
title: "Construire la structure de navigation",
status: TaskStatus.IN_PROGRESS,
priority: Priority.HIGH,
dueDate: new Date("2026-04-10"),
},
{
title: "Intégrer les notifications push",
status: TaskStatus.TODO,
priority: Priority.MEDIUM,
dueDate: new Date("2026-05-15"),
},
],
},
},
});
console.log("Projets seedés :", { webApp: webApp.id, mobileApp: mobileApp.id });
}
main()
.then(async () => {
await prisma.$disconnect();
})
.catch(async (e) => {
console.error(e);
await prisma.$disconnect();
process.exit(1);
});Ajoutez la commande de seed dans package.json :
{
"prisma": {
"seed": "npx tsx prisma/seed.ts"
}
}Exécutez le seed :
npx prisma db seedVous pouvez vérifier les données avec Prisma Studio :
npx prisma studioCela ouvre un navigateur visuel de base de données à http://localhost:5555.
Étape 8 : Construire les définitions de types et la validation
Créez src/lib/validations.ts pour les schémas Zod partagés :
import { z } from "zod";
export const createProjectSchema = z.object({
name: z.string().min(1, "Le nom est requis").max(100),
description: z.string().max(500).optional(),
color: z.string().regex(/^#[0-9a-fA-F]{6}$/, "Format de couleur invalide"),
});
export const createTaskSchema = z.object({
title: z.string().min(1, "Le titre est requis").max(200),
description: z.string().max(1000).optional(),
status: z.enum(["TODO", "IN_PROGRESS", "DONE"]).default("TODO"),
priority: z.enum(["LOW", "MEDIUM", "HIGH", "URGENT"]).default("MEDIUM"),
dueDate: z.string().optional(),
projectId: z.string().min(1, "Le projet est requis"),
});
export const updateTaskSchema = createTaskSchema.partial().extend({
id: z.string().min(1),
});
export type CreateProjectInput = z.infer<typeof createProjectSchema>;
export type CreateTaskInput = z.infer<typeof createTaskSchema>;
export type UpdateTaskInput = z.infer<typeof updateTaskSchema>;Étape 9 : Créer les Server Actions
Les Server Actions sont la méthode recommandée pour gérer les mutations dans Next.js App Router. Créez src/app/actions.ts :
"use server";
import { revalidatePath } from "next/cache";
import { prisma } from "@/lib/prisma";
import {
createProjectSchema,
createTaskSchema,
updateTaskSchema,
} from "@/lib/validations";
// ─── Actions Projets ─────────────────────────────────────
export async function createProject(formData: FormData) {
const raw = {
name: formData.get("name") as string,
description: formData.get("description") as string,
color: formData.get("color") as string,
};
const validated = createProjectSchema.safeParse(raw);
if (!validated.success) {
return { error: validated.error.flatten().fieldErrors };
}
await prisma.project.create({
data: validated.data,
});
revalidatePath("/");
return { success: true };
}
export async function deleteProject(id: string) {
await prisma.project.delete({
where: { id },
});
revalidatePath("/");
return { success: true };
}
// ─── Actions Tâches ───────────────────────────────────────
export async function createTask(formData: FormData) {
const raw = {
title: formData.get("title") as string,
description: formData.get("description") as string,
status: formData.get("status") as string,
priority: formData.get("priority") as string,
dueDate: formData.get("dueDate") as string,
projectId: formData.get("projectId") as string,
};
const validated = createTaskSchema.safeParse(raw);
if (!validated.success) {
return { error: validated.error.flatten().fieldErrors };
}
const { dueDate, ...rest } = validated.data;
await prisma.task.create({
data: {
...rest,
dueDate: dueDate ? new Date(dueDate) : null,
},
});
revalidatePath("/");
return { success: true };
}
export async function updateTaskStatus(id: string, status: string) {
const validated = updateTaskSchema.safeParse({ id, status });
if (!validated.success) {
return { error: "Entrée invalide" };
}
await prisma.task.update({
where: { id },
data: { status: validated.data.status },
});
revalidatePath("/");
return { success: true };
}
export async function deleteTask(id: string) {
await prisma.task.delete({
where: { id },
});
revalidatePath("/");
return { success: true };
}Les patterns clés ici :
"use server"marque le fichier comme contenant des Server Actions- Validation Zod assure l'intégrité des données avant toute opération en base
revalidatePath("/")vide le cache pour que l'interface se mette à jour immédiatement- Valeurs de retour typées permettent au client de gérer les états de succès et d'erreur
Étape 10 : Construire la page liste des projets
Créez la page principale dans src/app/page.tsx :
import Link from "next/link";
import { prisma } from "@/lib/prisma";
import { deleteProject } from "./actions";
export default async function HomePage() {
const projects = await prisma.project.findMany({
include: {
_count: {
select: { tasks: true },
},
tasks: {
select: { status: true },
},
},
orderBy: { createdAt: "desc" },
});
return (
<main className="max-w-4xl mx-auto p-8">
<div className="flex items-center justify-between mb-8">
<h1 className="text-3xl font-bold">Projets</h1>
<Link
href="/projects/new"
className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700"
>
Nouveau Projet
</Link>
</div>
<div className="grid gap-4">
{projects.map((project) => {
const done = project.tasks.filter((t) => t.status === "DONE").length;
const total = project._count.tasks;
const progress = total > 0 ? Math.round((done / total) * 100) : 0;
return (
<Link
key={project.id}
href={`/projects/${project.id}`}
className="block border rounded-lg p-6 hover:shadow-md transition-shadow"
>
<div className="flex items-start justify-between">
<div className="flex items-center gap-3">
<div
className="w-4 h-4 rounded-full"
style={{ backgroundColor: project.color }}
/>
<div>
<h2 className="text-xl font-semibold">{project.name}</h2>
{project.description && (
<p className="text-gray-500 mt-1">
{project.description}
</p>
)}
</div>
</div>
<span className="text-sm text-gray-400">
{total} tâche{total !== 1 ? "s" : ""}
</span>
</div>
{total > 0 && (
<div className="mt-4">
<div className="flex justify-between text-sm mb-1">
<span className="text-gray-500">Progression</span>
<span className="font-medium">{progress}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="h-2 rounded-full transition-all"
style={{
width: `${progress}%`,
backgroundColor: project.color,
}}
/>
</div>
</div>
)}
</Link>
);
})}
</div>
{projects.length === 0 && (
<div className="text-center py-12 text-gray-500">
<p className="text-lg">Aucun projet pour le moment</p>
<p className="mt-2">Créez votre premier projet pour commencer.</p>
</div>
)}
</main>
);
}Remarquez comment les requêtes Prisma s'exécutent directement dans le Server Component — pas besoin de routes API. L'option include vous permet de récupérer les données liées et les agrégats en une seule requête.
Étape 11 : Construire la page détail du projet
Créez src/app/projects/[id]/page.tsx :
import { notFound } from "next/navigation";
import { prisma } from "@/lib/prisma";
import { TaskList } from "./task-list";
import { TaskForm } from "./task-form";
interface Props {
params: Promise<{ id: string }>;
}
export default async function ProjectPage({ params }: Props) {
const { id } = await params;
const project = await prisma.project.findUnique({
where: { id },
include: {
tasks: {
orderBy: [
{ status: "asc" },
{ priority: "desc" },
{ createdAt: "desc" },
],
},
},
});
if (!project) {
notFound();
}
const tasksByStatus = {
TODO: project.tasks.filter((t) => t.status === "TODO"),
IN_PROGRESS: project.tasks.filter((t) => t.status === "IN_PROGRESS"),
DONE: project.tasks.filter((t) => t.status === "DONE"),
};
return (
<main className="max-w-5xl mx-auto p-8">
<div className="mb-8">
<div className="flex items-center gap-3 mb-2">
<div
className="w-5 h-5 rounded-full"
style={{ backgroundColor: project.color }}
/>
<h1 className="text-3xl font-bold">{project.name}</h1>
</div>
{project.description && (
<p className="text-gray-500 text-lg">{project.description}</p>
)}
</div>
<TaskForm projectId={project.id} />
<div className="grid md:grid-cols-3 gap-6 mt-8">
<TaskColumn title="À faire" tasks={tasksByStatus.TODO} color="#6b7280" />
<TaskColumn
title="En cours"
tasks={tasksByStatus.IN_PROGRESS}
color="#f59e0b"
/>
<TaskColumn title="Terminé" tasks={tasksByStatus.DONE} color="#10b981" />
</div>
</main>
);
}
function TaskColumn({
title,
tasks,
color,
}: {
title: string;
tasks: any[];
color: string;
}) {
return (
<div>
<div className="flex items-center gap-2 mb-4">
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: color }}
/>
<h2 className="font-semibold text-lg">{title}</h2>
<span className="text-sm text-gray-400 ml-auto">{tasks.length}</span>
</div>
<TaskList tasks={tasks} />
</div>
);
}Étape 12 : Construire les composants de tâches
Créez le composant liste de tâches dans src/app/projects/[id]/task-list.tsx :
"use client";
import { updateTaskStatus, deleteTask } from "@/app/actions";
import { useTransition } from "react";
interface Task {
id: string;
title: string;
description: string | null;
status: string;
priority: string;
dueDate: string | null;
}
const priorityColors: Record<string, string> = {
LOW: "bg-gray-100 text-gray-600",
MEDIUM: "bg-blue-100 text-blue-600",
HIGH: "bg-orange-100 text-orange-600",
URGENT: "bg-red-100 text-red-600",
};
export function TaskList({ tasks }: { tasks: Task[] }) {
return (
<div className="space-y-3">
{tasks.map((task) => (
<TaskCard key={task.id} task={task} />
))}
{tasks.length === 0 && (
<p className="text-gray-400 text-sm text-center py-4">Aucune tâche</p>
)}
</div>
);
}
function TaskCard({ task }: { task: Task }) {
const [isPending, startTransition] = useTransition();
const handleStatusChange = (newStatus: string) => {
startTransition(() => {
updateTaskStatus(task.id, newStatus);
});
};
const handleDelete = () => {
startTransition(() => {
deleteTask(task.id);
});
};
return (
<div
className={`border rounded-lg p-4 bg-white ${
isPending ? "opacity-50" : ""
}`}
>
<div className="flex items-start justify-between">
<h3 className="font-medium">{task.title}</h3>
<button
onClick={handleDelete}
className="text-gray-400 hover:text-red-500 text-sm"
>
×
</button>
</div>
{task.description && (
<p className="text-gray-500 text-sm mt-1">{task.description}</p>
)}
<div className="flex items-center gap-2 mt-3">
<span
className={`text-xs px-2 py-1 rounded-full ${
priorityColors[task.priority]
}`}
>
{task.priority}
</span>
{task.dueDate && (
<span className="text-xs text-gray-400">
Échéance {new Date(task.dueDate).toLocaleDateString("fr-FR")}
</span>
)}
</div>
<div className="flex gap-1 mt-3">
{["TODO", "IN_PROGRESS", "DONE"].map((status) => (
<button
key={status}
onClick={() => handleStatusChange(status)}
disabled={task.status === status}
className={`text-xs px-2 py-1 rounded ${
task.status === status
? "bg-gray-900 text-white"
: "bg-gray-100 hover:bg-gray-200 text-gray-600"
}`}
>
{status.replace("_", " ")}
</button>
))}
</div>
</div>
);
}Créez le formulaire de tâche dans src/app/projects/[id]/task-form.tsx :
"use client";
import { createTask } from "@/app/actions";
import { useRef } from "react";
import { useActionState } from "react";
const initialState = { error: null as any, success: false };
export function TaskForm({ projectId }: { projectId: string }) {
const formRef = useRef<HTMLFormElement>(null);
async function action(_prev: typeof initialState, formData: FormData) {
formData.set("projectId", projectId);
const result = await createTask(formData);
if (result.success) {
formRef.current?.reset();
}
return result as typeof initialState;
}
const [state, formAction, isPending] = useActionState(action, initialState);
return (
<form ref={formRef} action={formAction} className="border rounded-lg p-4">
<h3 className="font-semibold mb-3">Ajouter une nouvelle tâche</h3>
<div className="grid md:grid-cols-2 gap-3">
<input
name="title"
placeholder="Titre de la tâche"
required
className="border rounded px-3 py-2"
/>
<select name="priority" className="border rounded px-3 py-2">
<option value="LOW">Priorité basse</option>
<option value="MEDIUM" selected>Priorité moyenne</option>
<option value="HIGH">Priorité haute</option>
<option value="URGENT">Urgent</option>
</select>
<input
name="description"
placeholder="Description (optionnel)"
className="border rounded px-3 py-2"
/>
<input
name="dueDate"
type="date"
className="border rounded px-3 py-2"
/>
</div>
<input type="hidden" name="status" value="TODO" />
{state.error && (
<p className="text-red-500 text-sm mt-2">
{Object.values(state.error).flat().join(", ")}
</p>
)}
<button
type="submit"
disabled={isPending}
className="mt-3 bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 disabled:opacity-50"
>
{isPending ? "Ajout en cours..." : "Ajouter la tâche"}
</button>
</form>
);
}Étape 13 : Requêtes Prisma avancées
Prisma excelle dans les requêtes complexes. Voici des patterns que vous utiliserez fréquemment :
Filtrage et pagination
// Récupérer les tâches avec des filtres
const tasks = await prisma.task.findMany({
where: {
projectId: "some-id",
status: "IN_PROGRESS",
priority: { in: ["HIGH", "URGENT"] },
dueDate: { lte: new Date() }, // tâches en retard
},
orderBy: { priority: "desc" },
skip: 0,
take: 20,
});Agrégations
// Compter les tâches par statut par projet
const stats = await prisma.task.groupBy({
by: ["projectId", "status"],
_count: { id: true },
});Écritures imbriquées (Transactions)
// Créer un projet avec des tâches en une seule transaction
const project = await prisma.project.create({
data: {
name: "Nouveau Projet",
tasks: {
createMany: {
data: [
{ title: "Tâche 1", priority: "HIGH" },
{ title: "Tâche 2", priority: "MEDIUM" },
],
},
},
},
include: { tasks: true },
});Transactions interactives
// Transférer toutes les tâches d'un projet à un autre
await prisma.$transaction(async (tx) => {
const tasks = await tx.task.findMany({
where: { projectId: sourceId },
});
await tx.task.updateMany({
where: { projectId: sourceId },
data: { projectId: targetId },
});
await tx.project.delete({
where: { id: sourceId },
});
return tasks.length;
});Étape 14 : Ajouter la recherche et le filtrage
Créez un composant de recherche dans src/app/search.tsx :
"use client";
import { useRouter, useSearchParams } from "next/navigation";
import { useCallback } from "react";
export function SearchBar() {
const router = useRouter();
const searchParams = useSearchParams();
const updateFilter = useCallback(
(key: string, value: string) => {
const params = new URLSearchParams(searchParams.toString());
if (value) {
params.set(key, value);
} else {
params.delete(key);
}
router.push(`/?${params.toString()}`);
},
[router, searchParams]
);
return (
<div className="flex gap-3 mb-6">
<input
type="search"
placeholder="Rechercher des projets..."
defaultValue={searchParams.get("q") ?? ""}
onChange={(e) => updateFilter("q", e.target.value)}
className="border rounded px-3 py-2 flex-1"
/>
<select
defaultValue={searchParams.get("sort") ?? "newest"}
onChange={(e) => updateFilter("sort", e.target.value)}
className="border rounded px-3 py-2"
>
<option value="newest">Plus récents</option>
<option value="oldest">Plus anciens</option>
<option value="name">Par nom</option>
</select>
</div>
);
}Mettez à jour la page principale pour utiliser les paramètres de recherche :
interface Props {
searchParams: Promise<{ q?: string; sort?: string }>;
}
export default async function HomePage({ searchParams }: Props) {
const { q, sort } = await searchParams;
const projects = await prisma.project.findMany({
where: q
? {
OR: [
{ name: { contains: q, mode: "insensitive" } },
{ description: { contains: q, mode: "insensitive" } },
],
}
: undefined,
include: {
_count: { select: { tasks: true } },
tasks: { select: { status: true } },
},
orderBy:
sort === "name"
? { name: "asc" }
: sort === "oldest"
? { createdAt: "asc" }
: { createdAt: "desc" },
});
// ... reste du composant
}Étape 15 : Meilleures pratiques de production
Pooling de connexions
Pour les environnements serverless, configurez le pooling de connexions dans votre schéma :
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
directUrl = env("DIRECT_URL")
}Définissez DATABASE_URL vers une chaîne de connexion poolée (PgBouncer, pooler Neon, ou Prisma Accelerate) et DIRECT_URL vers la connexion directe (utilisée uniquement pour les migrations).
Prisma Accelerate
Pour les déploiements edge, activez Prisma Accelerate :
npm install @prisma/extension-accelerateMettez à jour votre client :
import { PrismaClient } from "@prisma/client";
import { withAccelerate } from "@prisma/extension-accelerate";
export const prisma = new PrismaClient().$extends(withAccelerate());Cela active le cache global et le pooling de connexions en edge.
Logging
Activez le logging des requêtes en développement :
export const prisma = new PrismaClient({
log:
process.env.NODE_ENV === "development"
? ["query", "error", "warn"]
: ["error"],
});Dépannage
"PrismaClientInitializationError: Can't reach database server"
- Vérifiez votre
DATABASE_URLdans.env - Assurez-vous que PostgreSQL est en cours d'exécution
- Vérifiez l'accès réseau (pare-feu, VPN)
Les types ne se mettent pas à jour après des changements de schéma
Exécutez la génération du client manuellement :
npx prisma generateCela régénère les types TypeScript depuis votre schéma.
"Unique constraint violation"
Vos données contiennent des valeurs dupliquées dans un champ unique. Soit mettez à jour l'enregistrement en conflit, soit utilisez upsert :
await prisma.project.upsert({
where: { id: "existing-id" },
update: { name: "Nom mis à jour" },
create: { name: "Nouveau Projet" },
});Dérive de migration en production
Si le schéma de votre base de données a divergé des migrations :
npx prisma migrate resolve --applied "migration_name"Ou réinitialiser (développement uniquement) :
npx prisma migrate resetProchaines étapes
Maintenant que vous avez une application full-stack fonctionnelle avec Prisma et Next.js, explorez ces sujets :
- Ajouter l'authentification avec NextAuth.js ou Better Auth pour scoper les projets par utilisateur
- Implémenter les mises à jour en temps réel avec Prisma Pulse pour des tableaux de tâches en direct
- Ajouter l'upload de fichiers pour les pièces jointes de tâches avec Vercel Blob ou S3
- Configurer le CI/CD avec GitHub Actions pour exécuter les migrations automatiquement
- Explorer les extensions Prisma Client pour le middleware, la suppression douce et le logging d'audit
Conclusion
Dans ce tutoriel, vous avez construit une application complète de gestion de projets avec Prisma ORM et Next.js 15 App Router. Vous avez appris à :
- Définir un modèle de données avec Prisma Schema Language incluant relations et enums
- Exécuter des migrations et seeder la base de données
- Créer un singleton Prisma Client pour Next.js
- Construire des Server Actions typées pour toutes les opérations CRUD
- Utiliser l'API de requête de Prisma pour le filtrage, la pagination et les agrégations
- Gérer les écritures imbriquées et les transactions pour les opérations complexes
- Implémenter la recherche et le filtrage avec les paramètres de recherche URL
- Configurer le déploiement en production avec le pooling de connexions et le support edge
Le schéma déclaratif de Prisma et les types auto-générés éliminent toute une catégorie de bugs — vous ne pouvez pas interroger un champ qui n'existe pas, passer le mauvais type, ou oublier une relation requise. Combiné avec les Server Components et Server Actions de Next.js, vous obtenez une expérience de développement full-stack typée de la base de données jusqu'à l'interface utilisateur.
Discutez de votre projet avec nous
Nous sommes ici pour vous aider avec vos besoins en développement Web. Planifiez un appel pour discuter de votre projet et comment nous pouvons vous aider.
Trouvons les meilleures solutions pour vos besoins.
Articles connexes

Recherche plein texte PostgreSQL avec Next.js — Construire une recherche puissante sans Elasticsearch (2026)
Apprenez à construire une recherche plein texte rapide et tolérante aux fautes de frappe en utilisant les capacités intégrées de PostgreSQL avec Next.js App Router. Pas besoin d'Elasticsearch ou d'Algolia — juste votre base de données Postgres existante.

Construire une Application Full-Stack avec Drizzle ORM et Next.js 15 : Base de Donnees Type-Safe du Zero a la Production
Apprenez a construire une application full-stack type-safe avec Drizzle ORM et Next.js 15. Ce tutoriel pratique couvre la conception de schemas, les migrations, les Server Actions, les operations CRUD et le deploiement avec PostgreSQL.

Neon Serverless Postgres avec Next.js App Router : construire une application full-stack avec le branching de base de données
Apprenez à construire une application Next.js full-stack alimentée par Neon Serverless Postgres. Ce tutoriel couvre le driver serverless Neon, le branching de base de données pour les déploiements preview, le connection pooling et les patterns prêts pour la production.