Construire une Application Full-Stack avec Drizzle ORM et Next.js 15 : Base de Donnees Type-Safe du Zero a la Production

Du SQL type-safe qui ressemble a du TypeScript. Drizzle ORM est le ORM moderne et leger qui vous donne toute la puissance de SQL sans aucune surcharge a l'execution. Dans ce tutoriel, vous allez construire une application complete de gestion de taches avec Next.js 15, les Server Actions et PostgreSQL.
Ce que vous allez apprendre
A la fin de ce tutoriel, vous serez capable de :
- Configurer Drizzle ORM avec PostgreSQL dans un projet Next.js 15
- Definir un schema de base de donnees type-safe en TypeScript pur
- Executer des migrations avec
drizzle-kit - Construire des operations CRUD completes avec les Server Actions de Next.js
- Gerer les soumissions de formulaires avec
useActionStateet la validation Zod - Implementer des requetes relationnelles avec le query builder de Drizzle
- Deployer une application prete pour la production avec des patterns de base de donnees solides
Prerequis
Avant de commencer, assurez-vous d'avoir :
- Node.js 20+ installe (
node --version) - Une experience en TypeScript (types, generiques, async/await)
- Une familiarite avec Next.js (App Router, Server Components)
- PostgreSQL en local ou une base de donnees cloud (nous utiliserons Neon)
- Un editeur de code — VS Code ou Cursor recommande
Pourquoi Drizzle ORM ?
L'ecosysteme JavaScript/TypeScript dispose de plusieurs ORM — Prisma, TypeORM, Sequelize — alors pourquoi choisir Drizzle ? Voici ce qui le distingue :
| Fonctionnalite | Drizzle | Prisma | TypeORM |
|---|---|---|---|
| Taille du bundle | ~7.4 KB | ~280 KB | ~180 KB |
| Langage du schema | TypeScript | DSL personnalise (.prisma) | TypeScript/Decorateurs |
| Controle SQL | API SQL-like complete | Requetes abstraites | Requetes abstraites |
| Pret pour le serverless | Oui (zero dependances) | Necessite un binaire moteur | Non optimise |
| Securite des types | Inference complete | Types generes | Partielle |
| Courbe d'apprentissage | Connaitre SQL = Connaitre Drizzle | Nouvelle syntaxe a apprendre | Patterns de decorateurs |
Drizzle adopte une approche fondamentalement differente : si vous connaissez SQL, vous connaissez deja Drizzle. Il n'y a pas de langage de requete personnalise, pas d'etape de generation de code, et pas de surcharge a l'execution. Votre schema est du TypeScript, vos requetes ressemblent a du SQL, et chaque type de retour est automatiquement infere.
Etape 1 : Creer un nouveau projet Next.js 15
Commencez par creer la structure d'un nouveau projet Next.js avec TypeScript :
npx create-next-app@latest task-manager --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"
cd task-managerSelectionnez les parametres par defaut lorsque demande. Cela vous donne un projet Next.js 15 avec :
- App Router
- TypeScript
- Tailwind CSS
- Structure de repertoire
src/
Etape 2 : Installer Drizzle ORM et les dependances
Installez Drizzle ORM, le driver PostgreSQL et Drizzle Kit (le CLI pour les migrations) :
npm install drizzle-orm @neondatabase/serverless
npm install -D drizzle-kitNous utilisons @neondatabase/serverless comme driver PostgreSQL car il fonctionne parfaitement dans les environnements serverless et traditionnels. Si vous utilisez un PostgreSQL local, vous pouvez utiliser postgres (postgres.js) ou pg a la place :
# Alternative : pour PostgreSQL local
npm install drizzle-orm postgres
npm install -D drizzle-kitEtape 3 : Configurer la connexion a la base de donnees
Creez un fichier .env.local avec votre chaine de connexion :
DATABASE_URL="postgresql://username:password@hostname/database?sslmode=require"Vous utilisez Neon ? Inscrivez-vous sur neon.tech, creez un projet et copiez la chaine de connexion depuis le tableau de bord. Le niveau gratuit vous offre 512 Mo — plus que suffisant pour ce tutoriel.
Maintenant, creez le fichier de connexion a la base de donnees :
// src/db/index.ts
import { drizzle } from "drizzle-orm/neon-http";
import { neon } from "@neondatabase/serverless";
import * as schema from "./schema";
const sql = neon(process.env.DATABASE_URL!);
export const db = drizzle({ client: sql, schema });Pour PostgreSQL local avec postgres (postgres.js), la configuration ressemble a ceci :
// src/db/index.ts (alternative pour PostgreSQL local)
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import * as schema from "./schema";
const client = postgres(process.env.DATABASE_URL!);
export const db = drizzle({ client, schema });Etape 4 : Definir votre schema de base de donnees
C'est ici que Drizzle brille. Votre schema est du TypeScript pur — pas de DSL personnalise, pas de decorateurs, juste des fonctions et des types :
// src/db/schema.ts
import {
pgTable,
serial,
text,
boolean,
timestamp,
integer,
pgEnum,
} from "drizzle-orm/pg-core";
// Definir un enum pour la priorite des taches
export const priorityEnum = pgEnum("priority", ["low", "medium", "high", "urgent"]);
// Table des utilisateurs
export const users = pgTable("users", {
id: serial("id").primaryKey(),
name: text("name").notNull(),
email: text("email").notNull().unique(),
createdAt: timestamp("created_at").defaultNow().notNull(),
});
// Table des projets
export const projects = pgTable("projects", {
id: serial("id").primaryKey(),
name: text("name").notNull(),
description: text("description"),
userId: integer("user_id")
.references(() => users.id, { onDelete: "cascade" })
.notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
});
// Table des taches
export const tasks = pgTable("tasks", {
id: serial("id").primaryKey(),
title: text("title").notNull(),
description: text("description"),
completed: boolean("completed").default(false).notNull(),
priority: priorityEnum("priority").default("medium").notNull(),
projectId: integer("project_id")
.references(() => projects.id, { onDelete: "cascade" })
.notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
});Remarquez comment cela se lit presque exactement comme des instructions SQL CREATE TABLE, mais avec une inference de types TypeScript complete.
Definir les relations
Drizzle separe les informations relationnelles du schema de la table. Cela garde les choses explicites :
// src/db/schema.ts (suite)
import { relations } from "drizzle-orm";
export const usersRelations = relations(users, ({ many }) => ({
projects: many(projects),
}));
export const projectsRelations = relations(projects, ({ one, many }) => ({
user: one(users, {
fields: [projects.userId],
references: [users.id],
}),
tasks: many(tasks),
}));
export const tasksRelations = relations(tasks, ({ one }) => ({
project: one(projects, {
fields: [tasks.projectId],
references: [projects.id],
}),
}));Etape 5 : Configurer Drizzle Kit
Creez le fichier de configuration Drizzle Kit a la racine du projet :
// drizzle.config.ts
import { defineConfig } from "drizzle-kit";
export default defineConfig({
schema: "./src/db/schema.ts",
out: "./drizzle",
dialect: "postgresql",
dbCredentials: {
url: process.env.DATABASE_URL!,
},
});Ajoutez les scripts de migration a votre package.json :
{
"scripts": {
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:push": "drizzle-kit push",
"db:studio": "drizzle-kit studio"
}
}Etape 6 : Generer et executer les migrations
Generez les fichiers de migration a partir de votre schema :
npm run db:generateCela cree des fichiers de migration SQL dans le repertoire drizzle/. Inspectez-les pour voir exactement quel SQL sera execute — Drizzle ne vous cache jamais le SQL.
Appliquez les migrations a votre base de donnees :
npm run db:migrateAstuce pour le developpement rapide : Utilisez npm run db:push pendant le developpement pour pousser les changements de schema directement sans generer de fichiers de migration. Utilisez db:generate + db:migrate pour les deploiements en production.
Etape 7 : Construire des Server Actions type-safe
Construisons maintenant les operations CRUD principales en utilisant les Server Actions de Next.js. Celles-ci s'executent sur le serveur et peuvent etre appelees directement depuis les composants React.
Action de creation de tache
// src/app/actions/tasks.ts
"use server";
import { db } from "@/db";
import { tasks } from "@/db/schema";
import { eq } from "drizzle-orm";
import { revalidatePath } from "next/cache";
import { z } from "zod";
// Schema de validation
const createTaskSchema = z.object({
title: z.string().min(1, "Le titre est requis").max(200),
description: z.string().optional(),
priority: z.enum(["low", "medium", "high", "urgent"]),
projectId: z.coerce.number().positive(),
});
export type ActionState = {
message: string;
errors?: Record<string, string[]>;
success?: boolean;
};
export async function createTask(
prevState: ActionState,
formData: FormData
): Promise<ActionState> {
const validatedFields = createTaskSchema.safeParse({
title: formData.get("title"),
description: formData.get("description"),
priority: formData.get("priority"),
projectId: formData.get("projectId"),
});
if (!validatedFields.success) {
return {
message: "Echec de la validation",
errors: validatedFields.error.flatten().fieldErrors,
};
}
try {
await db.insert(tasks).values(validatedFields.data);
revalidatePath("/dashboard");
return { message: "Tache creee avec succes", success: true };
} catch (error) {
return { message: "Echec de la creation de la tache. Veuillez reessayer." };
}
}Basculer l'etat d'achevement
// src/app/actions/tasks.ts (suite)
export async function toggleTask(taskId: number) {
const [task] = await db
.select({ completed: tasks.completed })
.from(tasks)
.where(eq(tasks.id, taskId));
if (!task) return;
await db
.update(tasks)
.set({
completed: !task.completed,
updatedAt: new Date(),
})
.where(eq(tasks.id, taskId));
revalidatePath("/dashboard");
}Supprimer une tache
// src/app/actions/tasks.ts (suite)
export async function deleteTask(taskId: number) {
await db.delete(tasks).where(eq(tasks.id, taskId));
revalidatePath("/dashboard");
}Etape 8 : Construire le composant de formulaire
Creez un composant de formulaire qui utilise useActionState pour une integration transparente avec les server actions :
// src/components/TaskForm.tsx
"use client";
import { useActionState } from "react";
import { createTask, type ActionState } from "@/app/actions/tasks";
const initialState: ActionState = { message: "" };
export function TaskForm({ projectId }: { projectId: number }) {
const [state, formAction, pending] = useActionState(createTask, initialState);
return (
<form action={formAction} className="space-y-4">
<input type="hidden" name="projectId" value={projectId} />
<div>
<label htmlFor="title" className="block text-sm font-medium">
Titre de la tache
</label>
<input
type="text"
id="title"
name="title"
required
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2"
placeholder="Que faut-il faire ?"
/>
{state.errors?.title && (
<p className="mt-1 text-sm text-red-600">{state.errors.title[0]}</p>
)}
</div>
<div>
<label htmlFor="description" className="block text-sm font-medium">
Description (optionnelle)
</label>
<textarea
id="description"
name="description"
rows={3}
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2"
placeholder="Ajoutez plus de details..."
/>
</div>
<div>
<label htmlFor="priority" className="block text-sm font-medium">
Priorite
</label>
<select
id="priority"
name="priority"
defaultValue="medium"
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2"
>
<option value="low">Basse</option>
<option value="medium">Moyenne</option>
<option value="high">Haute</option>
<option value="urgent">Urgente</option>
</select>
</div>
<button
type="submit"
disabled={pending}
className="rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 disabled:opacity-50"
>
{pending ? "Creation en cours..." : "Creer la tache"}
</button>
{state.message && !state.errors && (
<p
className={`text-sm ${state.success ? "text-green-600" : "text-red-600"}`}
aria-live="polite"
>
{state.message}
</p>
)}
</form>
);
}Etape 9 : Construire le tableau de bord avec les requetes relationnelles
Recuperons maintenant les donnees en utilisant la puissante API de requetes relationnelles de Drizzle :
// src/app/dashboard/page.tsx
import { db } from "@/db";
import { TaskForm } from "@/components/TaskForm";
import { TaskList } from "@/components/TaskList";
export default async function DashboardPage() {
// Requete relationnelle : obtenir les projets avec leurs taches
const projectsWithTasks = await db.query.projects.findMany({
with: {
tasks: {
orderBy: (tasks, { desc }) => [desc(tasks.createdAt)],
},
},
orderBy: (projects, { desc }) => [desc(projects.createdAt)],
});
return (
<main className="mx-auto max-w-4xl p-8">
<h1 className="mb-8 text-3xl font-bold">Gestionnaire de taches</h1>
{projectsWithTasks.map((project) => (
<section key={project.id} className="mb-8 rounded-lg border p-6">
<h2 className="mb-4 text-xl font-semibold">{project.name}</h2>
<p className="mb-4 text-gray-600">{project.description}</p>
<TaskList tasks={project.tasks} />
<TaskForm projectId={project.id} />
</section>
))}
</main>
);
}Etape 10 : Requetes avancees avec Drizzle
Drizzle prend en charge les operations SQL complexes avec une securite de types complete. Voici quelques patterns que vous utiliserez frequemment :
Requetes filtrees avec WHERE
import { eq, and, or, like, desc, asc, count, sql } from "drizzle-orm";
// Obtenir les taches incompletes de haute priorite
const urgentTasks = await db
.select()
.from(tasks)
.where(
and(
eq(tasks.completed, false),
or(eq(tasks.priority, "high"), eq(tasks.priority, "urgent"))
)
)
.orderBy(desc(tasks.createdAt));
// Rechercher des taches par titre
const searchResults = await db
.select()
.from(tasks)
.where(like(tasks.title, `%${searchTerm}%`));Agregations
// Compter les taches par projet
const taskCounts = await db
.select({
projectId: tasks.projectId,
projectName: projects.name,
total: count(),
completed: count(sql`CASE WHEN ${tasks.completed} THEN 1 END`),
})
.from(tasks)
.innerJoin(projects, eq(tasks.projectId, projects.id))
.groupBy(tasks.projectId, projects.name);Transactions
// Creer un projet avec des taches initiales de maniere atomique
import { db } from "@/db";
await db.transaction(async (tx) => {
const [project] = await tx
.insert(projects)
.values({ name: "Nouveau Projet", userId: 1 })
.returning();
await tx.insert(tasks).values([
{ title: "Configurer le depot", projectId: project.id, priority: "high" },
{ title: "Ecrire la documentation", projectId: project.id, priority: "medium" },
{ title: "Deployer en production", projectId: project.id, priority: "low" },
]);
});Etape 11 : Explorer avec Drizzle Studio
Drizzle Studio est une interface graphique integree pour la base de donnees qui vous permet de parcourir et modifier les donnees visuellement :
npm run db:studioCela ouvre une interface web locale sur https://local.drizzle.studio ou vous pouvez :
- Parcourir toutes les tables et les donnees
- Modifier les enregistrements en ligne
- Executer des requetes SQL brutes
- Visualiser les relations entre les tables
Etape 12 : Peupler la base de donnees
Creez un script de peuplement pour remplir votre base de donnees avec des donnees exemples :
// src/db/seed.ts
import { db } from "./index";
import { users, projects, tasks } from "./schema";
async function seed() {
console.log("Peuplement de la base de donnees...");
// Creer un utilisateur
const [user] = await db
.insert(users)
.values({ name: "Jean Dupont", email: "jean@example.com" })
.returning();
// Creer des projets
const [project1] = await db
.insert(projects)
.values([
{ name: "Refonte du site web", description: "Moderniser le site de l'entreprise", userId: user.id },
{ name: "Application mobile", description: "Construire l'application React Native", userId: user.id },
])
.returning();
// Creer des taches
await db.insert(tasks).values([
{ title: "Designer les maquettes", priority: "high", projectId: project1.id },
{ title: "Implementer la page d'accueil", priority: "medium", projectId: project1.id },
{ title: "Ajouter le mode sombre", priority: "low", projectId: project1.id },
{ title: "Ecrire les tests", priority: "high", projectId: project1.id },
]);
console.log("Peuplement termine !");
}
seed().catch(console.error);Executez-le :
npm run db:seedTester votre implementation
Demarrez le serveur de developpement et verifiez que tout fonctionne :
npm run devOuvrez http://localhost:3000/dashboard et testez :
- Voir les projets et les taches — les donnees se chargent depuis la base de donnees
- Creer une nouvelle tache — le formulaire est soumis via Server Action, la page est revalidee
- Basculer l'achevement — la case a cocher met a jour la base de donnees et l'interface
- Supprimer une tache — l'element est supprime de la base de donnees et de la liste
- Verifier Drizzle Studio — lancez
npm run db:studioen parallele pour voir les changements en temps reel
Depannage
"Cannot find module '@/db'"
Assurez-vous que votre tsconfig.json a le bon alias de chemin :
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
}
}"relation does not exist"
Vous devez d'abord executer les migrations. Lancez npm run db:push (pour le developpement) ou npm run db:generate && npm run db:migrate pour creer les tables.
"Type error: Column type mismatch"
Cela signifie generalement que votre schema TypeScript a diverge de la base de donnees reelle. Lancez npm run db:generate pour creer une nouvelle migration qui reconcilie les differences.
Prochaines etapes
Vous avez maintenant une base solide pour construire des applications full-stack type-safe avec Drizzle ORM et Next.js 15. Voici ce que vous pouvez faire ensuite :
- Ajouter l'authentification — integrez Better Auth ou NextAuth.js avec Drizzle comme adaptateur de base de donnees
- Ajouter des mises a jour en temps reel — utilisez Drizzle avec WebSockets ou server-sent events
- Implementer une UI optimiste — combinez
useOptimisticavec les Server Actions pour un retour instantane - Ajouter la recherche et le filtrage — utilisez les operateurs
like,ilikeet la recherche plein texte de Drizzle - Deployer sur Vercel — fonctionne directement avec Neon PostgreSQL
- Explorez la documentation Drizzle pour les fonctionnalites avancees
Conclusion
Drizzle ORM represente une nouvelle philosophie dans les ORM TypeScript : SQL n'est pas quelque chose dont il faut se cacher — c'est quelque chose a adopter avec la securite des types. Contrairement aux ORM qui inventent leurs propres langages de requetes, Drizzle se mappe directement aux concepts SQL que vous connaissez deja tout en vous donnant toute la puissance du systeme de types de TypeScript.
Dans ce tutoriel, vous avez construit une application complete de gestion de taches qui demontre :
- Le schema comme code — votre structure de base de donnees vit en TypeScript
- Zero surcharge a l'execution — Drizzle compile en SQL efficace
- Securite de types complete — du schema au resultat de la requete, chaque type est infere
- Patterns modernes — Server Actions,
useActionStateet validation Zod - Experience developpeur — Drizzle Studio, migrations et API SQL-first
La combinaison Drizzle ORM + Next.js 15 + PostgreSQL vous donne une stack prete pour la production, legere, type-safe et agreable a utiliser. Commencez a construire.
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

Construire une application temps réel avec Supabase et Next.js 15 : guide complet
Apprenez à construire une application full-stack en temps réel avec Supabase et Next.js 15 App Router. Ce guide couvre l'authentification, la base de données, Row Level Security et les abonnements temps réel.

Construire un Agent IA Autonome avec Agentic RAG et Next.js
Apprenez a construire un agent IA qui decide de maniere autonome quand et comment recuperer des informations depuis des bases de donnees vectorielles. Un guide pratique complet avec Vercel AI SDK et Next.js, accompagne d'exemples executables.

Authentifier votre application Next.js 15 avec Auth.js v5 : Email, OAuth et contrôle des rôles
Apprenez à ajouter une authentification prête pour la production à votre application Next.js 15 avec Auth.js v5. Ce guide complet couvre Google OAuth, les identifiants email/mot de passe, les routes protégées, le middleware et le contrôle d'accès basé sur les rôles.