ITSkillsCenter
Blog

Next.js 14 App Router : tutoriel complet

9 دقائق للقراءة

Ce que vous saurez faire à la fin

  1. Créer un projet Next.js 14 avec App Router structuré comme une vraie application e-commerce sénégalaise.
  2. Maîtriser les Server Components, Client Components, layouts imbriqués et streaming pour des temps de chargement sous 2 secondes même en 3G.
  3. Implémenter le data fetching côté serveur, la mise en cache native et la revalidation à la demande sans librairie tierce.
  4. Construire des Server Actions sécurisées pour traiter formulaires, paiements Wave et création de comptes.
  5. Déployer en production sur Vercel ou un VPS OVH à 6 000 FCFA/mois avec build optimisé et middleware d’authentification.

Durée : 5h. Pré-requis : Node.js 20+, React fonctionnel (hooks de base), JavaScript ES2022, Git, un compte GitHub gratuit. Coût : 0 à 6 000 FCFA/mois selon hébergement.

Étape 1 — Comprendre App Router vs Pages Router

Le Pages Router (présent depuis Next.js 1) place chaque route dans pages/ avec une convention très simple. L’App Router, stable depuis Next.js 13.4 et raffiné jusqu’à 14.2 puis 15.0, place les routes dans app/ et apporte les Server Components React, le streaming, les layouts imbriqués et les Server Actions.

Pages Router (legacy mais toujours supporté) :
pages/
  index.js
  produits/[id].js
  api/produits.js

App Router (recommandé pour tout nouveau projet) :
app/
  page.tsx                  -> route /
  produits/[id]/page.tsx    -> route /produits/123
  produits/layout.tsx       -> layout partagé
  api/produits/route.ts     -> route handler

Pour un nouveau projet PME en 2026, choisissez systématiquement App Router. Tout le développement de Vercel et de la communauté se concentre dessus.

Étape 2 — Créer le projet

npx create-next-app@14 boutique-dakar
# Répondre :
# TypeScript : Yes
# ESLint : Yes
# Tailwind CSS : Yes
# src/ directory : No
# App Router : Yes
# Import alias : @/*

cd boutique-dakar
npm run dev
# Ouvrir http://localhost:3000

Le squelette généré contient app/page.tsx (page d’accueil), app/layout.tsx (HTML racine), public/ pour les assets statiques. Tout l’arborescence est claire dès le premier coup d’œil.

Étape 3 — Server Components par défaut

Dans App Router, chaque composant est un Server Component sauf indication contraire. Cela signifie qu’il est rendu sur le serveur, ne pollue pas le bundle JS du navigateur, et peut accéder directement à la base de données ou aux variables d’environnement secrètes.

// app/produits/page.tsx
import { sql } from '@vercel/postgres';

export default async function ProduitsPage() {
  // Cette requête s'exécute SUR LE SERVEUR uniquement
  const { rows } = await sql`
    SELECT id, nom, prix_fcfa
    FROM produits
    WHERE actif = true
    ORDER BY created_at DESC
    LIMIT 20
  `;

  return (
    <main>
      <h1>Nos produits</h1>
      <ul>
        {rows.map(p => (
          <li key={p.id}>
            {p.nom} : {p.prix_fcfa.toLocaleString()} FCFA
          </li>
        ))}
      </ul>
    </main>
  );
}

Étape 4 — Client Components quand nécessaire

// app/components/PanierBouton.tsx
'use client';
import { useState } from 'react';

export default function PanierBouton({ produitId }: { produitId: number }) {
  const [count, setCount] = useState(0);
  return (
    <button
      onClick={() => setCount(count + 1)}
      className="rounded bg-emerald-600 px-4 py-2 text-white"
    >
      Ajouter au panier ({count})
    </button>
  );
}

La directive ‘use client’ en haut du fichier signale à Next.js que ce composant doit être hydraté côté navigateur. Règle : exclusivement pour les composants avec interactivité (onClick, useState, useEffect, formulaires complexes).

Étape 5 — Layouts imbriqués

// app/layout.tsx (racine)
export default function RootLayout({
  children
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="fr-SN">
      <body>
        <header>Boutique Dakar</header>
        {children}
        <footer>© 2026 Boutique Dakar SARL</footer>
      </body>
    </html>
  );
}

// app/admin/layout.tsx (imbriqué)
export default function AdminLayout({
  children
}: {
  children: React.ReactNode;
}) {
  return (
    <div className="flex">
      <aside className="w-64">Menu admin</aside>
      <section className="flex-1">{children}</section>
    </div>
  );
}

Quand un visiteur accède à /admin/produits, Next.js compose RootLayout + AdminLayout + page.tsx. Pas de code dupliqué, navigation SPA fluide.

Étape 6 — Routes dynamiques et paramètres

// app/produits/[slug]/page.tsx
import { notFound } from 'next/navigation';
import { sql } from '@vercel/postgres';

export default async function FicheProduit({
  params
}: {
  params: { slug: string };
}) {
  const { rows } = await sql`
    SELECT * FROM produits WHERE slug = ${params.slug}
  `;
  if (rows.length === 0) notFound();
  const p = rows[0];

  return (
    <article>
      <h1>{p.nom}</h1>
      <p>{p.description}</p>
      <strong>{p.prix_fcfa.toLocaleString()} FCFA</strong>
    </article>
  );
}

export async function generateMetadata({
  params
}: {
  params: { slug: string };
}) {
  const { rows } = await sql`
    SELECT nom, description FROM produits WHERE slug = ${params.slug}
  `;
  return { title: rows[0]?.nom, description: rows[0]?.description };
}

Étape 7 — Streaming et Suspense

// app/dashboard/page.tsx
import { Suspense } from 'react';
import VentesJour from './VentesJour';
import StockBas from './StockBas';
import TopProduits from './TopProduits';

export default function Dashboard() {
  return (
    <main>
      <h1>Tableau de bord</h1>
      <Suspense fallback={<p>Chargement ventes...</p>}>
        <VentesJour />
      </Suspense>
      <Suspense fallback={<p>Chargement stock...</p>}>
        <StockBas />
      </Suspense>
      <Suspense fallback={<p>Chargement top...</p>}>
        <TopProduits />
      </Suspense>
    </main>
  );
}

Chaque section se charge en parallèle et s’affiche dès qu’elle est prête. Sur une connexion 3G à Saint-Louis, le visiteur voit le titre et le squelette en 400 ms au lieu d’attendre 4 secondes le rendu complet.

Étape 8 — Cache et revalidation

// Fetch avec cache automatique (par défaut)
const res = await fetch('https://api.fournisseur.sn/produits', {
  next: { revalidate: 3600 } // ISR : recache toutes les heures
});

// Fetch sans cache (toujours frais)
const res2 = await fetch('https://api.wave.com/balance', {
  cache: 'no-store'
});

// Revalidation à la demande depuis une Server Action
import { revalidatePath, revalidateTag } from 'next/cache';

export async function publierArticle(formData: FormData) {
  await sql`INSERT INTO articles ...`;
  revalidatePath('/blog'); // recache la liste
  revalidateTag('articles'); // invalide tag
}

Étape 9 — Server Actions pour formulaires

// app/contact/page.tsx
import { redirect } from 'next/navigation';

async function envoyerContact(formData: FormData) {
  'use server';
  const nom = formData.get('nom')?.toString() ?? '';
  const message = formData.get('message')?.toString() ?? '';

  if (nom.length < 2 || message.length < 10) {
    throw new Error('Données invalides');
  }

  await fetch('https://api.brevo.com/v3/smtp/email', {
    method: 'POST',
    headers: {
      'api-key': process.env.BREVO_KEY!,
      'content-type': 'application/json'
    },
    body: JSON.stringify({
      to: [{ email: 'contact@boutique-dakar.sn' }],
      subject: `Message de ${nom}`,
      htmlContent: message
    })
  });

  redirect('/contact/merci');
}

export default function Contact() {
  return (
    <form action={envoyerContact}>
      <input name="nom" required minLength={2} />
      <textarea name="message" required minLength={10} />
      <button type="submit">Envoyer</button>
    </form>
  );
}

Étape 10 — Middleware d’authentification

// middleware.ts (à la racine du projet)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const session = request.cookies.get('session_token');

  // Protéger /admin/*
  if (request.nextUrl.pathname.startsWith('/admin')) {
    if (!session) {
      const url = new URL('/login', request.url);
      url.searchParams.set('redirect', request.nextUrl.pathname);
      return NextResponse.redirect(url);
    }
  }

  return NextResponse.next();
}

export const config = {
  matcher: ['/admin/:path*', '/dashboard/:path*']
};

Étape 11 — API Routes (Route Handlers)

// app/api/wave/webhook/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { sql } from '@vercel/postgres';
import crypto from 'node:crypto';

export async function POST(request: NextRequest) {
  const signature = request.headers.get('wave-signature');
  const body = await request.text();

  // Vérifier signature HMAC Wave
  const expected = crypto
    .createHmac('sha256', process.env.WAVE_SECRET!)
    .update(body)
    .digest('hex');

  if (signature !== expected) {
    return NextResponse.json({ erreur: 'Signature invalide' }, { status: 401 });
  }

  const evt = JSON.parse(body);
  if (evt.type === 'checkout.session.completed') {
    await sql`
      UPDATE commandes
      SET statut = 'payee'
      WHERE wave_session_id = ${evt.id}
    `;
  }

  return NextResponse.json({ ok: true });
}

Étape 12 — Variables d’environnement et secrets

# .env.local (jamais commité)
DATABASE_URL=postgresql://user:pass@host/db
WAVE_SECRET=whsec_xxxxx
BREVO_KEY=xkeysib-xxxxx
NEXT_PUBLIC_GA_ID=G-XXXX

# .env.example (commité, sans valeurs)
DATABASE_URL=
WAVE_SECRET=
BREVO_KEY=
NEXT_PUBLIC_GA_ID=

Préfixez NEXT_PUBLIC_ pour les variables exposées au navigateur (analytics, clés publiques Mapbox). Tout le reste reste secret côté serveur.

Étape 13 — Build et déploiement Vercel

npm run build
# Vérifier la sortie : taille des routes, RSC, serverless

# Option 1 : Vercel (gratuit jusqu'à 100 Go/mois)
npm install -g vercel
vercel --prod

# Option 2 : VPS OVH 6 000 FCFA/mois
# Sur le serveur (Ubuntu 22.04) :
curl -fsSL https://nodejs.org/dist/v20.18.0/node-v20.18.0-linux-x64.tar.xz | tar -xJ
git clone https://github.com/vous/boutique-dakar.git
cd boutique-dakar
npm install --production
npm run build
npm install -g pm2
pm2 start npm --name boutique -- start
pm2 startup && pm2 save

Étape 14 — Optimisation images et fonts

// app/page.tsx
import Image from 'next/image';
import { Inter } from 'next/font/google';

const inter = Inter({ subsets: ['latin'], display: 'swap' });

export default function Home() {
  return (
    <main className={inter.className}>
      <Image
        src="/hero-dakar.jpg"
        alt="Marché de Sandaga"
        width={1200}
        height={600}
        priority
        sizes="(max-width: 768px) 100vw, 1200px"
      />
    </main>
  );
}

next/image génère automatiquement WebP et AVIF, redimensionne à la volée et applique du lazy loading. Une page e-commerce avec 30 photos passe de 12 Mo à 1,8 Mo de transfert.

Erreurs fréquentes

  • useState dans un Server Component : erreur « useState is not a function ». Ajoutez ‘use client’ en haut du fichier.
  • fetch infini : oubli de cache: ‘no-store’ alors que la donnée doit changer. Le cache par défaut est trop agressif pour des données utilisateur.
  • Variables NEXT_PUBLIC_ exposées : tout ce qui est préfixé est visible dans le bundle. N’y mettez jamais de secret.
  • Hydration mismatch : rendre une date avec new Date() côté serveur et navigateur produit deux valeurs différentes. Utilisez suppressHydrationWarning ou rendez la date côté client uniquement.
  • Server Actions sans validation : n’importe qui peut envoyer une requête forgée. Validez toujours avec Zod ou Valibot avant insertion en base.

Checklist de validation

  • Projet Next.js 14 créé avec App Router et TypeScript
  • Au moins 3 routes dont une dynamique avec [slug]
  • Layout racine + un layout imbriqué fonctionnels
  • Un Server Component qui interroge une base de données
  • Un Client Component avec interactivité (formulaire ou bouton)
  • Suspense boundary autour d’un composant lent pour streaming
  • Une Server Action sécurisée traitant un formulaire
  • Middleware d’authentification protégeant /admin
  • Variables d’environnement séparées (.env.local et .env.example)
  • Build de production sans warnings, taille de bundle inspectée
  • Déploiement Vercel ou VPS effectué et site accessible
  • Lighthouse score supérieur à 85 sur mobile depuis pagespeed.web.dev
Besoin d'un site web ?

Confiez-nous la Création de Votre Site Web

Site vitrine, e-commerce ou application web — nous transformons votre vision en réalité digitale. Accompagnement personnalisé de A à Z.

À partir de 250.000 FCFA
Parlons de Votre Projet
Publicité