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

Noqta TeamAI Bot
Par Noqta Team & AI Bot ·

Chargement du lecteur de synthèse vocale...

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 useActionState et 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 :

FonctionnaliteDrizzlePrismaTypeORM
Taille du bundle~7.4 KB~280 KB~180 KB
Langage du schemaTypeScriptDSL personnalise (.prisma)TypeScript/Decorateurs
Controle SQLAPI SQL-like completeRequetes abstraitesRequetes abstraites
Pret pour le serverlessOui (zero dependances)Necessite un binaire moteurNon optimise
Securite des typesInference completeTypes generesPartielle
Courbe d'apprentissageConnaitre SQL = Connaitre DrizzleNouvelle syntaxe a apprendrePatterns 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-manager

Selectionnez 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-kit

Nous 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-kit

Etape 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:generate

Cela 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:migrate

Astuce 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:studio

Cela 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:seed

Tester votre implementation

Demarrez le serveur de developpement et verifiez que tout fonctionne :

npm run dev

Ouvrez http://localhost:3000/dashboard et testez :

  1. Voir les projets et les taches — les donnees se chargent depuis la base de donnees
  2. Creer une nouvelle tache — le formulaire est soumis via Server Action, la page est revalidee
  3. Basculer l'achevement — la case a cocher met a jour la base de donnees et l'interface
  4. Supprimer une tache — l'element est supprime de la base de donnees et de la liste
  5. Verifier Drizzle Studio — lancez npm run db:studio en 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 useOptimistic avec les Server Actions pour un retour instantane
  • Ajouter la recherche et le filtrage — utilisez les operateurs like, ilike et 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, useActionState et 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.


Vous voulez lire plus de tutoriels? Découvrez notre dernier tutoriel sur Maitriser Framer Motion : Guide Complet pour des Animations Epoustouflantes.

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 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.

30 min read·