Développement Web

Comprendre app router vs pages router

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

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 (informations vérifiées en avril 2026, susceptibles d’évoluer), 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

Next.js App Router — la transition de 2024-2026

Next.js 13 (octobre 2022) introduit App Router, et Next.js 15 (octobre 2024) confirme qu il est la voie standard. Pages Router reste supporte pour les projets historiques, mais les nouveautes (React Server Components, streaming, partial rendering) sont reservees a App Router. En 2026, tout nouveau projet Next.js doit demarrer en App Router.

Difference fondamentale : Pages Router rend tout cote serveur ou tout cote client par page. App Router permet le mixage fin — chaque composant peut etre Server (rendu serveur, zero JavaScript client) ou Client (interactif, JavaScript client). Le poids du bundle JavaScript livre au navigateur est divise par 5-10 sur un site standard, ce qui ameliore drastiquement les Core Web Vitals.

React Server Components (RSC) — le concept central

Par defaut, tout composant dans /app est un Server Component. Il peut faire des fetch, lire des fichiers, interroger une base de donnees directement — sans api intermediaire. Le HTML rendu serveur est envoye au navigateur, qui n a aucun JavaScript a charger pour ces composants. Le terme appropriate : zero hydration cost.

Pour qu un composant soit interactif (gere onClick, useState, useEffect), il doit etre marque comme Client Component via la directive ‘use client’ en tete de fichier. Le Server Component peut importer des Client Components et leur passer des props, mais l inverse n est pas possible.

Cette architecture permet de minimiser le JavaScript client. Un site editorial typique peut avoir 90 pour cent de Server Components et 10 pour cent de Client Components (boutons, formulaires, recherches), pour un bundle initial de 30-80 Ko au lieu de 200-500 Ko en Pages Router.

Layouts et Loading UI

App Router introduit deux conventions tres utiles. Le fichier layout.tsx dans un dossier definit le layout commun aux pages enfants. Imbrique automatiquement : / -> layout principal, /dashboard -> layout dashboard, /dashboard/settings -> layout settings. Permet de creer des UIs hierarchiques avec persistance d etat (un menu lateral reste monte quand on change de page).

Le fichier loading.tsx dans un dossier affiche un fallback (typiquement un skeleton) pendant que la page chargie. C est Suspense gere par convention. Reduit la perception du temps d attente — un visiteur voit l interface immediatement, le contenu se remplit progressivement.

Quand basculer un projet Pages Router vers App Router ?

Si le projet est petit (< 50 pages), une reecriture complete en 1-2 semaines vaut le coup et donne les benefices immediatement. Si le projet est gros (> 100 pages), migration progressive page-par-page possible (App et Pages peuvent cohabiter dans le meme projet). Si le projet est tres mature et stable, ne pas migrer juste par mode — peser le cout/benefice contre les vrais ROI (performance, DX, nouveautes).

Pieges typiques de la migration

Mauvaise utilisation de ‘use client’. Ajouter ‘use client’ partout par precaution annule les benefices de RSC. La discipline : par defaut Server, basculer Client uniquement pour les composants reellement interactifs.

Fetching duplique. Plusieurs composants qui fetchent les memes donnees. La solution : fetch deduplique automatiquement les requetes identiques au sein d un render (cache memoize). Pour des donnees largement partagees, utiliser React cache ou un store comme Zustand.

useEffect avec fetch. Pattern Pages Router courant. En App Router, faire le fetch directement dans le Server Component est plus rapide et plus simple. useEffect reste utile pour les interactions DOM ou les abonnements, pas pour le chargement de donnees initiales.

References

مشاركة