Introduction
En 2026, les plateformes de formation en ligne exigent des systèmes dynamiques pour gérer des plans de formation modulaires, avec suivi de progression et personnalisation. Ce tutoriel expert vous guide pour implémenter un gestionnaire complet en Next.js 15+ (App Router), Prisma pour l'ORM, Zod pour la validation et React Query pour le caching.
Pourquoi c'est crucial ? Les plans de formation ne sont plus statiques : ils intègrent des modules hiérarchiques (plan → module → leçon), des métadonnées (durée, niveau) et une API REST scalable. Vous apprendrez à modéliser une base de données relationnelle, à sécuriser les endpoints avec authentification basique, et à créer une UI réactive.
À la fin, vous aurez un projet fonctionnel, déployable sur Vercel, gérant 1000+ plans sans latence. Idéal pour les leads devs construisant des LMS (Learning Management Systems). Temps estimé : 2h pour un expert.
Prérequis
- Node.js 20+ et npm/yarn/pnpm
- Connaissances avancées en TypeScript, Next.js App Router et Prisma
- Outils : VS Code avec extensions Prisma et TypeScript
- Git pour versionning
- Base SQLite (pour dev) ou PostgreSQL (prod)
Initialisation du projet Next.js
npx create-next-app@latest plan-formation-app --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"
cd plan-formation-app
npm install prisma @prisma/client zod @tanstack/react-query
npm install -D prismaCette commande crée un projet Next.js 15+ optimisé pour TypeScript et Tailwind. On ajoute Prisma pour l'ORM, Zod pour valider les schémas API, et React Query pour le state management asynchrone. Évitez les templates legacy : App Router est obligatoire pour les performances 2026.
Configuration de la base de données
Avant de modéliser, configurez Prisma. Créez un fichier .env avec DATABASE_URL="file:./dev.db" pour SQLite en dev. Les relations seront PlanFormation 1:N Module, Module 1:N Lecon. Utilisez des enums pour niveau (DEBUTANT, EXPERT) et statut (EN_COURS, TERMINE).
Schéma Prisma complet
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
model PlanFormation {
id String @id @default(cuid())
titre String
description String?
niveau Niveau @default(DEBUTANT)
duree Int // en heures
modules Module[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Module {
id String @id @default(cuid())
titre String
ordre Int
planId String
plan PlanFormation @relation(fields: [planId], references: [id], onDelete: Cascade)
lecons Lecon[]
createdAt DateTime @default(now())
}
model Lecon {
id String @id @default(cuid())
titre String
contenu String // Markdown ou vidéo URL
duree Int
moduleId String
module Module @relation(fields: [moduleId], references: [id], onDelete: Cascade)
ordre Int
}
enum Niveau {
DEBUTANT
INTERMEDIAIRE
EXPERT
}Ce schéma définit une hiérarchie complète : plans contiennent modules, modules contiennent leçons. Utilisez cuid() pour IDs distribués, Cascade pour suppression propre. Piège : oubliez @updatedAt pour audits ; passez à PostgreSQL en prod pour scaling.
Génération et migration Prisma
npx prisma generate
npx prisma db push
npx prisma studioGénérez le client Prisma, synchronisez la DB avec db push (plus rapide que migrate pour dev), et ouvrez Studio pour inspecter. En prod, utilisez prisma migrate dev. Évitez prisma db pull sans backup : cela écrase les schémas.
Implémentation des routes API
Créez des routes REST dans app/api. Utilisez Zod pour valider les payloads, Prisma pour queries optimisées (select/include pour éviter N+1), et error handling avec NextResponse. Pour expert : ajoutez caching avec revalidatePath et headers sécurisés.
API GET tous les plans (avec modules)
import { NextResponse } from 'next/server';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export async function GET() {
try {
const plans = await prisma.planFormation.findMany({
select: {
id: true,
titre: true,
description: true,
niveau: true,
duree: true,
modules: {
select: {
id: true,
titre: true,
ordre: true,
lecons: {
select: {
id: true,
titre: true,
duree: true,
},
},
},
orderBy: { ordre: 'asc' },
},
},
orderBy: { createdAt: 'desc' },
});
return NextResponse.json(plans);
} catch (error) {
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
}
}Cette route fetch tous les plans avec modules et leçons nested via select pour optimiser (pas de N+1). orderBy trie logiquement. Piège expert : sans select, vous chargez tout ; testez avec 1000+ records pour perf.
API POST créer un plan
import { NextResponse } from 'next/server';
import { z } from 'zod';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
const createSchema = z.object({
titre: z.string().min(3),
description: z.string().optional(),
niveau: z.enum(['DEBUTANT', 'INTERMEDIAIRE', 'EXPERT']).default('DEBUTANT'),
duree: z.number().min(1),
});
export async function POST(request: Request) {
try {
const body = await request.json();
const data = createSchema.parse(body);
const plan = await prisma.planFormation.create({ data });
return NextResponse.json(plan, { status: 201 });
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json({ error: error.errors }, { status: 400 });
}
return NextResponse.json({ error: 'Erreur création' }, { status: 500 });
}
}Validation stricte avec Zod parse ; création atomique. Erreur Zod renvoyée proprement. Ajoutez auth middleware en prod (NextAuth). Piège : sans try/catch sur Prisma, les transactions échouent silencieusement.
Composant liste des plans avec React Query
import { useQuery } from '@tanstack/react-query';
const fetchPlans = async () => {
const res = await fetch('/api/plans');
if (!res.ok) throw new Error('Erreur fetch');
return res.json();
};
export default function PlansPage() {
const { data: plans, isLoading, error } = useQuery({
queryKey: ['plans'],
queryFn: fetchPlans,
});
if (isLoading) return <div>Chargement...</div>;
if (error) return <div>Erreur: {error.message}</div>;
return (
<div className="p-8 max-w-4xl mx-auto">
<h1 className="text-3xl font-bold mb-8">Plans de Formation</h1>
<div className="grid gap-6 md:grid-cols-2">
{plans?.map((plan: any) => (
<div key={plan.id} className="border p-6 rounded-lg shadow">
<h2 className="text-2xl font-semibold">{plan.titre}</h2>
<p className="text-gray-600">{plan.description}</p>
<p>Niveau: {plan.niveau} | Durée: {plan.duree}h</p>
<details>
<summary>Modules ({plan.modules.length})</summary>
<ul className="mt-2 ml-4">
{plan.modules.map((m: any) => (
<li key={m.id}>{m.titre} ({m.lecons.length} leçons)</li>
))}
</ul>
</details>
</div>
))}
</div>
</div>
);
}React Query gère fetch, cache et refetch auto. UI Tailwind responsive avec nested details. Piège : sans queryKey unique, cache corrompu ; ajoutez staleTime: 5 60 1000 pour prod.
Provider React Query dans layout
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import './globals.css';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5,
retry: 2,
},
},
});
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="fr">
<body>
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
</body>
</html>
);
}Layout global wrappe avec QueryClient configuré (staleTime 5min, retry). DevTools pour debug. Essentiel pour apps expert : centralise le state async sans Redux boilerplate.
Bonnes pratiques
- Optimisez queries Prisma : toujours
selectexplicite, indexes surordreetplanId. - Sécurisez API : ajoutez NextAuth v5 et rôles (admin pour POST).
- Caching avancé :
revalidateTagsur mutations, SWR pour edge cases. - Tests : Jest + MSW pour API mocks, Playwright pour E2E.
- Déploiement : Vercel avec Prisma Accelerate pour DB globale.
Erreurs courantes à éviter
- Oublier
prisma.$disconnect()en dev : leaks mémoire (utilisez singleton global). - N+1 queries : sans
include/select, perf chute à 10 req/module. - Validation laxiste : Zod prévient injections ; sans, SQLi via Prisma raw.
- Pas de transactions : pour créer plan+modules atomique, wrappez en
prisma.$transaction().
Pour aller plus loin
Étendez avec suivi progression (modèle UserProgression), IA pour recommandations (OpenAI API), ou PWA pour offline.
Découvrez nos formations Learni sur Next.js et Prisma pour maîtriser les architectures fullstack avancées. Ressources : Docs Prisma Relations, React Query Guide.