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.
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...).
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.
-
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 : tapepnpm -v. Un numĂ©ro de version s'affiche (ex.9.12.0) â sinon l'installation a Ă©chouĂ©. -
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 : tapels. Tu dois voir apparaĂźtreapp/,package.json,tailwind.config.tsâ la commande a rempli ton dossier de fichiers. -
Initialise shadcn/ui
pnpm dlx shadcn@latest init
Ăa crĂ©ecomponents.jsonetlib/utils.ts, et prĂ©pare Tailwind pour recevoir des composants shadcn/ui prĂȘts Ă copier.đ Comment voir le rĂ©sultat : tapels components.json lib/utils.tsâ les deux fichiers doivent exister, sans message d'erreur « No such file ». -
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 : tapels libâ les dossiersauth,db,validationsdoivent apparaĂźtre. -
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 : tapegit log --onelineâ une ligne avec ton message de commit doit s'afficher. -
Lance le serveur et vérifie
pnpm dev
Ouvrehttp://localhost:3000. Si tu as une erreur 500 mentionnant un module introuvable (ex.Can't resolve 'xxx') alors qu'il est bien danspackage.json: supprimenode_modules,pnpm-lock.yamlet.next, relancepnpm installpuispnpm dev. C'est presque toujours une installation incomplĂšte, pas un bug de code.đ Comment voir le rĂ©sultat : dans ton navigateur, ouvrelocalhost:3000. Tu dois voir la page d'accueil par dĂ©faut de Next.js (logo, liens « Docs », « Deploy »). Le terminal afficheReadyet n'affiche pas d'erreur rouge.
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.
-
CrĂ©e un projet sur SupabaseVa 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).
-
RĂ©cupĂšre l'URL de connexionDans le projet Supabase : Project Settings â Database â Connection string, mode URI. Copie l'URL complĂšte (mot de passe inclus).
-
Installe Prisma
pnpm add prisma --save-dev pnpm add @prisma/client pnpm dlx prisma init
CrĂ©eprisma/schema.prismaet un fichier.envavec une variableDATABASE_URLvide.đ Comment voir le rĂ©sultat : tapels prisma/schema.prismaâ le fichier doit exister. -
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.gitignorepar dĂ©faut) â il contient un mot de passe. -
Ăcris le schĂ©maDans
prisma/schema.prisma, sous les blocsgenerator/datasourcedĂ©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. -
Applique la migration et vérifie
pnpm dlx prisma migrate dev --name init pnpm dlx prisma studio
migrate devcrĂ©e les tables et gĂ©nĂšre le client Prisma typĂ©.studioouvre une interface web locale pour voir/Ă©diter les donnĂ©es Ă la main.đ Comment voir le rĂ©sultat :prisma studioouvrelocalhost:5555dans ton navigateur â la tableProduitdoit apparaĂźtre dans la liste de gauche, vide mais avec ses colonnes (nom, quantite, prix...).
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.
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.
-
Installe Better Auth
pnpm add better-auth
đ Comment voir le rĂ©sultat :better-authapparaĂźt danspackage.json. -
Crée le client Prisma partagé
touch lib/db/client.tsNew-Item -ItemType File -Force -Path lib/db/client.ts
Le dossierlib/dbexiste 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 dePrismaClient, rĂ©utilisĂ©e partout viaimport { 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). -
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 }, }); -
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ĂšlesUser,Session,Accountdansschema.prisma.đ Comment voir le rĂ©sultat : ouvreprisma studioâ les tablesUser,Session,Accountdoivent maintenant apparaĂźtre dans la liste. -
Ajoute le secret dans
.env# .env BETTER_AUTH_SECRET="génÚre une chaßne aléatoire longue" BETTER_AUTH_URL="http://localhost:3000"
-
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 : relancepnpm devet ouvrelocalhost:3000/api/auth/ok(ou tout endpoint Better Auth valide) â tu dois obtenir une rĂ©ponse JSON, pas une erreur 404. -
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/signupet/loginvont utiliser pour appelersignUp.email(...)etsignIn.email(...). -
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 : ouvrelocalhost:3000/signup, crĂ©e un compte test. Puis ouvreprisma studioâ une ligne doit apparaĂźtre dans la tableUser. Reconnecte-toi ensuite depuis/loginavec les mĂȘmes identifiants. -
ProtĂšge
/dashboardtouch 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*"] };Lematcherlimite 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 surlocalhost:3000/dashboardâ tu dois ĂȘtre redirigĂ© vers/login, pas voir le contenu du dashboard.
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.
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).
-
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.tsxdoivent apparaĂźtre. -
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. -
Ă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. -
Construis la page
/dashboard/produitstouch 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 Componentasyncâprisma.produit.findMany()s'exĂ©cute directement cĂŽtĂ© serveur, pas besoin deuseEffect. 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 : ouvrelocalhost: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.
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.
-
Comprends oĂč vit le styleIl n'y a pas un fichier
.csspar page. Deux niveaux seulement : les variables de thĂšme globales dansapp/globals.css(couleurs, arrondi â gĂ©nĂ©rĂ©es parshadcn initĂ l'Ă©tape 1, chargĂ©es une seule fois parapp/layout.tsx), et les classes Tailwind Ă©crites directement dans leclassNamede chaque composant. -
Personnalise les couleurs du thĂšmeOuvre
app/globals.cssâ repĂšre le bloc:root { ... }gĂ©nĂ©rĂ© parshadcn init, qui contient des variables comme--primaryou--background. Change leurs valeurs directement dans ce fichier.đ Comment voir le rĂ©sultat : modifie la valeur de--primary, enregistre, rechargelocalhost:3000/dashboard/produitsâ n'importe quel composant shadcn/ui (leButtonajoutĂ© Ă l'Ă©tape 4) doit changer de couleur automatiquement, sans toucher au code des pages. -
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 fichierpage.tsxdu dashboard â un seul endroit Ă modifier si tu changes la largeur ou les marges.đ Comment voir le rĂ©sultat : enveloppe le contenu retournĂ© parapp/dashboard/produits/page.tsxavec<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. -
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>
InputetButtonont Ă©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 : rechargelocalhost: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. -
Pour modifier le style plus tardGarde
pnpm devlancĂ© 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 dansclassName, 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.
DĂ©ploiement â Vercel
Une URL publique et fonctionnelle.
Prérequis : étape 5 validée.
-
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. -
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. -
Importe le projet sur VercelSur 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.
-
Renseigne les variables d'environnementCopie
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. -
DĂ©ploie et vĂ©rifie en ligneOuvre 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.