Skip to content
Learni
Voir tous les tutoriels
Développement Fullstack

Comment implémenter un système de plans de formation en Next.js 2026

Read in English

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

terminal
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 prisma

Cette 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

prisma/schema.prisma
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

terminal
npx prisma generate
npx prisma db push
npx prisma studio

Gé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)

app/api/plans/route.ts
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

app/api/plans/route.ts
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

src/app/plans/page.tsx
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

src/app/layout.tsx
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 select explicite, indexes sur ordre et planId.
  • Sécurisez API : ajoutez NextAuth v5 et rôles (admin pour POST).
  • Caching avancé : revalidateTag sur 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.