Mode d'emploi — de zĂ©ro Ă  dĂ©ployĂ©

Backoffice

Un outil interne pour gĂ©rer des donnĂ©es — exemple : un catalogue produits. Base de donnĂ©es, CRUD, accĂšs protĂ©gĂ© si besoin.

Sommaire
01Initialiser le projet Next.js
02Base de donnĂ©es — Prisma + Supabase
03Authentification — Better Auth
04CRUD Produits
05Habillage — Tailwind & shadcn/ui
06DĂ©ploiement — Vercel
RĂšgle du jeu : tu tapes chaque commande toi-mĂȘme dans ton terminal, dans l'ordre. Tu ne passes Ă  l'Ă©tape suivante que quand la case « vĂ©rification » est cochĂ©e. Si une commande plante, copie-colle l'erreur exacte — on corrige ensemble, tu ne devines pas.

RĂ©utilisable vs spĂ©cifique : les Ă©tapes rĂ©utilisable sont valables pour n'importe quel projet de ta stack. Les Ă©tapes spĂ©cifique sont propres Ă  ce modĂšle « catalogue produits » — remplace Produit par tes propres donnĂ©es (clients, articles, stock...).
📖 C'est quoi chaque techno (Prisma, Supabase, Better Auth...) ? Le lexique complet est sur la page d'accueil →
01
Réutilisable

Initialiser le projet Next.js

Un projet Next.js qui dĂ©marre en local, avec Tailwind et shadcn/ui prĂȘts Ă  l'emploi.

Prérequis : Node.js installé. Terminal ouvert dans le dossier du projet.

  1. Installe pnpm si besoin
    npm install -g pnpm
    pnpm est le gestionnaire de paquets choisi pour ce projet — plus rapide et plus Ă©conome en espace disque que npm.
    👁 Comment voir le rĂ©sultat : tape pnpm -v. Un numĂ©ro de version s'affiche (ex. 9.12.0) — sinon l'installation a Ă©chouĂ©.
  2. Crée le projet Next.js
    pnpm create next-app@latest . --typescript --tailwind --eslint --app --src-dir=false --import-alias "@/*"
    Le nom du dossier ne doit pas contenir de majuscules (contrainte npm). S'il en contient, crée le projet dans un dossier temporaire au nom valide puis déplace tout son contenu dans ton dossier de travail.
    👁 Comment voir le rĂ©sultat : tape ls. Tu dois voir apparaĂźtre app/, package.json, tailwind.config.ts — la commande a rempli ton dossier de fichiers.
  3. Initialise shadcn/ui
    pnpm dlx shadcn@latest init
    Ça crĂ©e components.json et lib/utils.ts, et prĂ©pare Tailwind pour recevoir des composants shadcn/ui prĂȘts Ă  copier.
    👁 Comment voir le rĂ©sultat : tape ls components.json lib/utils.ts — les deux fichiers doivent exister, sans message d'erreur « No such file ».
  4. Crée l'arborescence du projet
    mkdir -p lib/auth lib/db lib/validations prisma testsmkdir lib/auth, lib/db, lib/validations, prisma, tests
    On sĂ©pare dĂšs le dĂ©part : authentification, accĂšs base de donnĂ©es, schĂ©mas de validation. Ça Ă©vite le fichier fourre-tout de 2000 lignes plus tard.
    👁 Comment voir le rĂ©sultat : tape ls lib — les dossiers auth, db, validations doivent apparaĂźtre.
  5. Initialise Git et fais le premier commit
    git init
    git add -A
    git commit -m "chore: init projet Next.js"
    👁 Comment voir le rĂ©sultat : tape git log --oneline — une ligne avec ton message de commit doit s'afficher.
  6. Lance le serveur et vérifie
    pnpm dev
    Ouvre http://localhost:3000. Si tu as une erreur 500 mentionnant un module introuvable (ex. Can't resolve 'xxx') alors qu'il est bien dans package.json : supprime node_modules, pnpm-lock.yaml et .next, relance pnpm install puis pnpm dev. C'est presque toujours une installation incomplĂšte, pas un bug de code.
    👁 Comment voir le rĂ©sultat : dans ton navigateur, ouvre localhost:3000. Tu dois voir la page d'accueil par dĂ©faut de Next.js (logo, liens « Docs », « Deploy »). Le terminal affiche Ready et n'affiche pas d'erreur rouge.
Vérification
02
Réutilisable

Base de donnĂ©es — Prisma + Supabase

Une base PostgreSQL en ligne, avec une table Produit créée via Prisma.

Prérequis : étape 1 validée.

  1. Crée un projet sur Supabase
    Va sur supabase.com → New project. Choisis un nom, une rĂ©gion proche, et un mot de passe de base de donnĂ©es — note-le quelque part.
    👁 Comment voir le rĂ©sultat : le tableau de bord Supabase affiche ton projet avec un statut « Active » (aprĂšs ~1-2 minutes d'initialisation).
  2. RécupÚre l'URL de connexion
    Dans le projet Supabase : Project Settings → Database → Connection string, mode URI. Copie l'URL complùte (mot de passe inclus).
  3. Installe Prisma
    pnpm add prisma --save-dev
    pnpm add @prisma/client
    pnpm dlx prisma init
    Crée prisma/schema.prisma et un fichier .env avec une variable DATABASE_URL vide.
    👁 Comment voir le rĂ©sultat : tape ls prisma/schema.prisma — le fichier doit exister.
  4. Colle l'URL dans .env
    # .env
    DATABASE_URL="postgresql://postgres:[MOT-DE-PASSE]@[HOST]:5432/postgres"
    Ce fichier ne doit jamais ĂȘtre commit (dĂ©jĂ  dans .gitignore par dĂ©faut) — il contient un mot de passe.
  5. Écris le schĂ©ma
    Dans prisma/schema.prisma, sous les blocs generator/datasource déjà présents, ajoute :
    model Produit {
      id        String   @id @default(cuid())
      nom       String
      quantite  Int      @default(0)
      prix      Float
      categorie String?
      updatedAt DateTime @updatedAt
    }
    Remplace par les champs de tes propres donnĂ©es — un backoffice de clients, d'articles, de commandes suivra exactement le mĂȘme schĂ©ma de raisonnement.
  6. Applique la migration et vérifie
    pnpm dlx prisma migrate dev --name init
    pnpm dlx prisma studio
    migrate dev crée les tables et génÚre le client Prisma typé. studio ouvre une interface web locale pour voir/éditer les données à la main.
    👁 Comment voir le rĂ©sultat : prisma studio ouvre localhost:5555 dans ton navigateur — la table Produit doit apparaĂźtre dans la liste de gauche, vide mais avec ses colonnes (nom, quantite, prix...).
Vérification

PiĂšge frĂ©quent : connexion refusĂ©e → vĂ©rifie qu'aucun caractĂšre spĂ©cial du mot de passe (@, #...) n'est mal encodĂ© dans l'URL. Supabase propose un bouton pour copier l'URL dĂ©jĂ  encodĂ©e.

03
Réutilisable

Authentification — Better Auth

S'inscrire, se connecter, et protéger /dashboard des visiteurs non connectés.

Prérequis : étape 2 validée (schéma Prisma en place). Si l'outil est utilisé par toute l'équipe sans distinction d'accÚs, tu peux sauter cette étape.

  1. Installe Better Auth
    pnpm add better-auth
    👁 Comment voir le rĂ©sultat : better-auth apparaĂźt dans package.json.
  2. Crée le client Prisma partagé
    touch lib/db/client.tsNew-Item -ItemType File -Force -Path lib/db/client.ts
    Le dossier lib/db existe dĂ©jĂ  depuis l'Ă©tape 1 — il ne manque que le fichier. Ouvre-le et colle ce contenu :
    // lib/db/client.ts
    import { PrismaClient } from "@prisma/client";
    
    const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };
    
    export const prisma = globalForPrisma.prisma ?? new PrismaClient();
    
    if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
    Une instance unique de PrismaClient, rĂ©utilisĂ©e partout via import { prisma } from "@/lib/db/client" — Ă©vite d'en recrĂ©er une Ă  chaque appel (et les erreurs « too many connections » que ça provoque avec le rechargement Ă  chaud de Next.js en dĂ©veloppement).
  3. Crée la config serveur
    touch lib/auth/auth.tsNew-Item -ItemType File -Force -Path lib/auth/auth.ts
    // lib/auth/auth.ts
    import { betterAuth } from "better-auth";
    import { prismaAdapter } from "better-auth/adapters/prisma";
    import { prisma } from "@/lib/db/client";
    
    export const auth = betterAuth({
      database: prismaAdapter(prisma, { provider: "postgresql" }),
      emailAndPassword: { enabled: true },
    });
  4. Ajoute les tables auth au schéma
    pnpm dlx @better-auth/cli generate
    pnpm dlx prisma migrate dev --name add_auth
    Better Auth génÚre automatiquement les modÚles User, Session, Account dans schema.prisma.
    👁 Comment voir le rĂ©sultat : ouvre prisma studio — les tables User, Session, Account doivent maintenant apparaĂźtre dans la liste.
  5. Ajoute le secret dans .env
    # .env
    BETTER_AUTH_SECRET="génÚre une chaßne aléatoire longue"
    BETTER_AUTH_URL="http://localhost:3000"
  6. Crée la route API
    mkdir -p "app/api/auth/[...all]" && touch "app/api/auth/[...all]/route.ts"New-Item -ItemType Directory -Force -Path "app/api/auth/[...all]"; New-Item -ItemType File -Force -Path "app/api/auth/[...all]/route.ts"
    Le nom de dossier [...all] (avec les crochets) est la syntaxe Next.js pour capturer toutes les sous-routes — ne le renomme pas.
    // app/api/auth/[...all]/route.ts
    import { auth } from "@/lib/auth/auth";
    import { toNextJsHandler } from "better-auth/next-js";
    
    export const { GET, POST } = toNextJsHandler(auth);
    👁 Comment voir le rĂ©sultat : relance pnpm dev et ouvre localhost:3000/api/auth/ok (ou tout endpoint Better Auth valide) — tu dois obtenir une rĂ©ponse JSON, pas une erreur 404.
  7. Crée le client auth
    touch lib/auth/client.tsNew-Item -ItemType File -Force -Path lib/auth/client.ts
    // lib/auth/client.ts
    import { createAuthClient } from "better-auth/react";
    
    export const authClient = createAuthClient();
    C'est ce client (cÎté navigateur) que les pages /signup et /login vont utiliser pour appeler signUp.email(...) et signIn.email(...).
  8. Crée les pages d'inscription et de connexion
    mkdir -p app/signup app/login && touch app/signup/page.tsx app/login/page.tsxmkdir app/signup, app/login; New-Item -ItemType File -Force -Path app/signup/page.tsx, app/login/page.tsx
    // app/signup/page.tsx
    "use client";
    import { useState } from "react";
    import { authClient } from "@/lib/auth/client";
    
    export default function SignupPage() {
      const [email, setEmail] = useState("");
      const [password, setPassword] = useState("");
    
      async function handleSubmit(e: React.FormEvent) {
        e.preventDefault();
        await authClient.signUp.email({ email, password, name: email });
      }
    
      return (
        <form onSubmit={handleSubmit} className="p-8 flex flex-col gap-2 max-w-sm">
          <input type="email" placeholder="Email" value={email} onChange={(e) => setEmail(e.target.value)} required />
          <input type="password" placeholder="Mot de passe" value={password} onChange={(e) => setPassword(e.target.value)} required />
          <button type="submit">Créer un compte</button>
        </form>
      );
    }
    // app/login/page.tsx
    "use client";
    import { useState } from "react";
    import { authClient } from "@/lib/auth/client";
    
    export default function LoginPage() {
      const [email, setEmail] = useState("");
      const [password, setPassword] = useState("");
    
      async function handleSubmit(e: React.FormEvent) {
        e.preventDefault();
        await authClient.signIn.email({ email, password });
      }
    
      return (
        <form onSubmit={handleSubmit} className="p-8 flex flex-col gap-2 max-w-sm">
          <input type="email" placeholder="Email" value={email} onChange={(e) => setEmail(e.target.value)} required />
          <input type="password" placeholder="Mot de passe" value={password} onChange={(e) => setPassword(e.target.value)} required />
          <button type="submit">Se connecter</button>
        </form>
      );
    }
    👁 Comment voir le rĂ©sultat : ouvre localhost:3000/signup, crĂ©e un compte test. Puis ouvre prisma studio — une ligne doit apparaĂźtre dans la table User. Reconnecte-toi ensuite depuis /login avec les mĂȘmes identifiants.
  9. ProtĂšge /dashboard
    touch middleware.tsNew-Item -ItemType File -Force -Path middleware.ts
    // middleware.ts (à la racine du projet, à cÎté de package.json)
    import { NextRequest, NextResponse } from "next/server";
    import { auth } from "@/lib/auth/auth";
    
    export async function middleware(request: NextRequest) {
      const session = await auth.api.getSession({ headers: request.headers });
      if (!session && request.nextUrl.pathname.startsWith("/dashboard")) {
        return NextResponse.redirect(new URL("/login", request.url));
      }
      return NextResponse.next();
    }
    
    export const config = { matcher: ["/dashboard/:path*"] };
    Le matcher limite le middleware aux routes /dashboard/* — il ne s'exĂ©cute pas sur le reste du site.
    👁 Comment voir le rĂ©sultat : ouvre un navigateur en navigation privĂ©e (pas connectĂ©) et va sur localhost:3000/dashboard — tu dois ĂȘtre redirigĂ© vers /login, pas voir le contenu du dashboard.
Vérification

PiĂšge frĂ©quent : si la doc officielle de Better Auth diffĂšre de ce qui est Ă©crit ici, suis la doc — c'est une librairie qui Ă©volue vite. Dis-le moi si un nom de fonction a changĂ©, on ajuste.

04
Spécifique backoffice

CRUD Produits

Créer, lister, modifier et supprimer un produit depuis /dashboard.

Prérequis : étape 3 validée (ou étape 2 si tu as sauté l'authentification).

  1. Ajoute des composants shadcn/ui
    pnpm dlx shadcn@latest add button input table
    👁 Comment voir le rĂ©sultat : ls components/ui — button.tsx, input.tsx, table.tsx doivent apparaĂźtre.
  2. Valide les entrées avec Zod
    touch lib/validations/produit.tsNew-Item -ItemType File -Force -Path lib/validations/produit.ts
    // lib/validations/produit.ts
    import { z } from "zod";
    export const produitSchema = z.object({
      nom: z.string().min(1).max(80),
      quantite: z.number().int().min(0),
      prix: z.number().positive(),
      categorie: z.string().optional(),
    });
    👁 Comment voir le rĂ©sultat : pas d'effet visible seul — ce schĂ©ma sera testĂ© concrĂštement Ă  l'Ă©tape suivante, en soumettant un formulaire invalide.
  3. Écris les Server Actions
    mkdir -p app/dashboard/produits && touch app/dashboard/produits/actions.tsmkdir app/dashboard/produits; New-Item -ItemType File -Force -Path app/dashboard/produits/actions.ts
    // app/dashboard/produits/actions.ts
    "use server";
    import { prisma } from "@/lib/db/client";
    import { produitSchema } from "@/lib/validations/produit";
    import { revalidatePath } from "next/cache";
    
    function readForm(formData: FormData) {
      return produitSchema.parse({
        nom: formData.get("nom"),
        quantite: Number(formData.get("quantite")),
        prix: Number(formData.get("prix")),
        categorie: formData.get("categorie") || undefined,
      });
    }
    
    export async function createProduit(formData: FormData) {
      await prisma.produit.create({ data: readForm(formData) });
      revalidatePath("/dashboard/produits");
    }
    
    export async function updateProduit(id: string, formData: FormData) {
      await prisma.produit.update({ where: { id }, data: readForm(formData) });
      revalidatePath("/dashboard/produits");
    }
    
    export async function deleteProduit(id: string) {
      await prisma.produit.delete({ where: { id } });
      revalidatePath("/dashboard/produits");
    }
    Si tu as activĂ© Better Auth, vĂ©rifie la session avant chaque Ă©criture (auth.api.getSession(...)) — sinon n'importe qui avec l'URL peut modifier les donnĂ©es. produitSchema.parse(...) lĂšve une erreur si un champ est invalide, ce qui bloque l'Ă©criture.
  4. Construis la page /dashboard/produits
    touch app/dashboard/produits/page.tsxNew-Item -ItemType File -Force -Path app/dashboard/produits/page.tsx
    // app/dashboard/produits/page.tsx
    import { prisma } from "@/lib/db/client";
    import { createProduit, deleteProduit } from "./actions";
    
    export default async function ProduitsPage() {
      const produits = await prisma.produit.findMany();
    
      return (
        <main className="p-8">
          <h1 className="text-2xl font-bold mb-4">Produits</h1>
    
          <form action={createProduit} className="flex gap-2 mb-6">
            <input name="nom" placeholder="Nom" required />
            <input name="quantite" type="number" placeholder="Quantité" required />
            <input name="prix" type="number" step="0.01" placeholder="Prix" required />
            <button type="submit">Ajouter</button>
          </form>
    
          <table>
            <tbody>
              {produits.map((p) => (
                <tr key={p.id}>
                  <td>{p.nom}</td>
                  <td>{p.quantite}</td>
                  <td>{p.prix} €</td>
                  <td>
                    <form action={deleteProduit.bind(null, p.id)}>
                      <button type="submit">Supprimer</button>
                    </form>
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        </main>
      );
    }
    La page est un Server Component async — prisma.produit.findMany() s'exĂ©cute directement cĂŽtĂ© serveur, pas besoin de useEffect. Le formulaire de modification suit le mĂȘme principe qu'un formulaire de suppression, avec un champ cachĂ© pour l'id.
    👁 Comment voir le rĂ©sultat : ouvre localhost:3000/dashboard/produits, ajoute un produit test via le formulaire — il doit apparaĂźtre dans le tableau sans recharger la page manuellement. VĂ©rifie aussi dans Prisma Studio que la ligne existe bien en base.
Vérification
05
Réutilisable

Habillage — Tailwind & shadcn/ui

Un rendu visuel cohérent sur tout le dashboard, et une méthode simple pour le modifier plus tard.

Prérequis : étape 4 validée.

  1. Comprends oĂč vit le style
    Il n'y a pas un fichier .css par page. Deux niveaux seulement : les variables de thĂšme globales dans app/globals.css (couleurs, arrondi — gĂ©nĂ©rĂ©es par shadcn init Ă  l'Ă©tape 1, chargĂ©es une seule fois par app/layout.tsx), et les classes Tailwind Ă©crites directement dans le className de chaque composant.
  2. Personnalise les couleurs du thĂšme
    Ouvre app/globals.css — repĂšre le bloc :root { ... } gĂ©nĂ©rĂ© par shadcn init, qui contient des variables comme --primary ou --background. Change leurs valeurs directement dans ce fichier.
    👁 Comment voir le rĂ©sultat : modifie la valeur de --primary, enregistre, recharge localhost:3000/dashboard/produits — n'importe quel composant shadcn/ui (le Button ajoutĂ© Ă  l'Ă©tape 4) doit changer de couleur automatiquement, sans toucher au code des pages.
  3. Crée une mise en page cohérente, réutilisée sur chaque page du dashboard
    touch components/container.tsxNew-Item -ItemType File -Force -Path components/container.tsx
    // components/container.tsx
    export default function Container({ children }: { children: React.ReactNode }) {
      return (
        <div className="max-w-5xl mx-auto px-4 py-12">
          {children}
        </div>
      );
    }
    Sans ça, tu rĂ©pĂštes les mĂȘmes classes d'espacement dans chaque fichier page.tsx du dashboard — un seul endroit Ă  modifier si tu changes la largeur ou les marges.
    👁 Comment voir le rĂ©sultat : enveloppe le contenu retournĂ© par app/dashboard/produits/page.tsx avec <Container>...</Container> (Ă  la place de <main className="p-8">) — la page doit s'afficher centrĂ©e, avec la mĂȘme marge, une fois rechargĂ©e.
  4. Remplace le HTML brut par les composants shadcn/ui déjà installés
    // avant — app/dashboard/produits/page.tsx
    <input name="nom" placeholder="Nom" required />
    <button type="submit">Ajouter</button>
    
    // aprĂšs
    import { Input } from "@/components/ui/input";
    import { Button } from "@/components/ui/button";
    <Input name="nom" placeholder="Nom" required />
    <Button type="submit">Ajouter</Button>
    Input et Button ont Ă©tĂ© installĂ©s Ă  l'Ă©tape 4 (shadcn add button input table) mais jamais utilisĂ©s dans le JSX — ils hĂ©ritent automatiquement des variables de thĂšme dĂ©finies plus haut, contrairement Ă  un <input>/<button> HTML brut.
    👁 Comment voir le rĂ©sultat : recharge localhost:3000/dashboard/produits — le champ et le bouton doivent avoir le style shadcn/ui (bordure arrondie, couleur du thĂšme), pas l'apparence par dĂ©faut du navigateur.
  5. Pour modifier le style plus tard
    Garde pnpm dev lancĂ© en continu — le rechargement Ă  chaud applique tes changements dans le navigateur dĂšs que tu sauvegardes, sans redĂ©marrer le serveur. Ouvre le fichier de la page ou du composant concernĂ©, change les classes dans className, enregistre. Pour ajouter un nouveau composant shadcn/ui : pnpm dlx shadcn@latest add <nom> (liste sur ui.shadcn.com). Pour savoir ce que fait une classe (p-4, rounded-lg, text-xl...), consulte le lexique ou tailwindcss.com/docs.
Vérification
06
Réutilisable

DĂ©ploiement — Vercel

Une URL publique et fonctionnelle.

Prérequis : étape 5 validée.

  1. Ajoute la génération Prisma au build
    // package.json
    "scripts": { "postinstall": "prisma generate" }
    Sans ça, le build Vercel plante — le client Prisma doit ĂȘtre rĂ©gĂ©nĂ©rĂ© aprĂšs chaque install en production.
  2. Pousse le code sur GitHub
    git remote add origin URL-DE-TON-DEPOT
    git push -u origin master
    👁 Comment voir le rĂ©sultat : rafraĂźchis la page du dĂ©pĂŽt sur GitHub — tes fichiers doivent y apparaĂźtre.
  3. Importe le projet sur Vercel
    Sur vercel.com → Add New → Project → sĂ©lectionne le dĂ©pĂŽt GitHub.
    👁 Comment voir le rĂ©sultat : le tableau de bord Vercel passe de « Building » Ă  « Ready », avec un lien de prĂ©visualisation cliquable.
  4. Renseigne les variables d'environnement
    Copie DATABASE_URL, BETTER_AUTH_SECRET, BETTER_AUTH_URL (mets l'URL Vercel finale) dans Settings → Environment Variables.
    👁 Comment voir le rĂ©sultat : pas d'effet visible immĂ©diatement — redĂ©ploie (« Redeploy ») puis teste la connexion en production Ă  l'Ă©tape suivante.
  5. Déploie et vérifie en ligne
    Ouvre l'URL fournie par Vercel, refais le parcours critique à la main : créer un produit, le modifier, le supprimer.
    👁 Comment voir le rĂ©sultat : sur l'URL *.vercel.app, connecte-toi, ajoute un produit — il doit apparaĂźtre dans le tableau exactement comme en local.
Vérification