Intermediaire 11 min de lecture · 2 318 mots

Next.js 14+ : SSR, SSG, et App Router

Estimated reading time: 11 minutes

Introduction

Next.js 14+ révolutionne le développement React avec l’App Router, les Server Components, et des performances exceptionnelles. Découvrez comment créer des applications ultra-rapides avec les stratégies de rendu modernes.

1. Créer un projet Next.js 14+

Installation

# Créer un nouveau projet
npx create-next-app@latest mon-app-nextjs

# Options recommandées
✔ Would you like to use TypeScript? … Yes
✔ Would you like to use ESLint? … Yes
✔ Would you like to use Tailwind CSS? … Yes
✔ Would you like to use src/ directory? … Yes
✔ Would you like to use App Router? … Yes
✔ Would you like to customize the default import alias (@/)? … Yes

# Naviguer et lancer
cd mon-app-nextjs
npm run dev

Structure de projet avec App Router

mon-app-nextjs/
├── src/
│   ├── app/
│   │   ├── layout.tsx          # Layout racine
│   │   ├── page.tsx            # Page d'accueil
│   │   ├── globals.css         # Styles globaux
│   │   ├── api/                # API Routes
│   │   │   └── users/
│   │   │       └── route.ts
│   │   ├── blog/               # Route /blog
│   │   │   ├── page.tsx
│   │   │   ├── [slug]/         # Route dynamique /blog/[slug]
│   │   │   │   └── page.tsx
│   │   │   └── loading.tsx     # Loading UI
│   │   ├── dashboard/
│   │   │   ├── layout.tsx      # Layout imbriqué
│   │   │   ├── page.tsx
│   │   │   └── error.tsx       # Error UI
│   │   └── not-found.tsx       # 404 page
│   ├── components/
│   │   ├── Header.tsx
│   │   └── Footer.tsx
│   └── lib/
│       └── api.ts
├── public/
│   ├── images/
│   └── favicon.ico
├── next.config.js
├── tsconfig.json
└── package.json

2. App Router : Routing basé sur le système de fichiers

Pages de base

// src/app/page.tsx - Route: /
export default function HomePage() {
  return (
    
# Bienvenue

Page d'accueil

); } // src/app/about/page.tsx - Route: /about export default function AboutPage() { return (
# À propos
); } // src/app/blog/page.tsx - Route: /blog export default function BlogPage() { return (
# Blog
); }

Routes dynamiques

// src/app/blog/[slug]/page.tsx - Route: /blog/mon-article
interface PageProps {
  params: {
    slug: string;
  };
  searchParams: {
    [key: string]: string | string[] | undefined;
  };
}

export default function BlogPostPage({ params, searchParams }: PageProps) {
  return (
    
# Article: {params.slug}

Query params: {JSON.stringify(searchParams)}

); } // src/app/shop/[category]/[product]/page.tsx // Route: /shop/electronics/laptop interface ProductPageProps { params: { category: string; product: string; }; } export default function ProductPage({ params }: ProductPageProps) { return (
# Catégorie: {params.category} ## Produit: {params.product}
); }

Catch-all routes

// src/app/docs/[...slug]/page.tsx
// Matche: /docs/a, /docs/a/b, /docs/a/b/c
interface DocsPageProps {
  params: {
    slug: string[];
  };
}

export default function DocsPage({ params }: DocsPageProps) {
  return (
    
# Documentation

Path: {params.slug.join('/')}

); } // src/app/shop/[[...slug]]/page.tsx // Matche: /shop, /shop/a, /shop/a/b (optional catch-all)

Route Groups (organisation sans impact sur l’URL)

src/app/
├── (marketing)/          # Groupe sans effet sur l'URL
│   ├── layout.tsx        # Layout partagé
│   ├── page.tsx          # Route: /
│   ├── about/
│   │   └── page.tsx      # Route: /about
│   └── contact/
│       └── page.tsx      # Route: /contact
├── (shop)/
│   ├── layout.tsx
│   ├── products/
│   │   └── page.tsx      # Route: /products
│   └── cart/
│       └── page.tsx      # Route: /cart
└── dashboard/
    └── page.tsx          # Route: /dashboard

3. Layouts et Templates

Layout racine

// src/app/layout.tsx
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import './globals.css';

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

export const metadata: Metadata = {
  title: {
    default: 'Mon App',
    template: '%s | Mon App'
  },
  description: 'Application Next.js moderne',
  keywords: ['Next.js', 'React', 'TypeScript']
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    
      
        
{children}
Footer global
); }

Layouts imbriqués

// src/app/dashboard/layout.tsx
export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    
  );
}

// src/app/dashboard/page.tsx
export default function DashboardPage() {
  return # Dashboard;
}
// Render: RootLayout > DashboardLayout > DashboardPage

Templates (réinitialisation du state)

// src/app/dashboard/template.tsx
// Contrairement au layout, le template crée une nouvelle instance à chaque navigation
export default function DashboardTemplate({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    
{children}
); }

4. Server Components vs Client Components

Server Components (par défaut)

// src/app/blog/page.tsx
// Server Component par défaut - pas de 'use client'

async function getPosts() {
  const res = await fetch('https://api.example.com/posts', {
    next: { revalidate: 60 } // ISR: revalide toutes les 60 secondes
  });
  return res.json();
}

export default async function BlogPage() {
  const posts = await getPosts();

  return (
    
# Blog {posts.map((post: any) => (
## {post.title}

{post.excerpt}

))}
); }

Client Components

// src/components/Counter.tsx
'use client'; // Directive obligatoire pour les Client Components

import { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);

  return (
    

Count: {count}

); }

Composition Server + Client

// src/app/blog/[slug]/page.tsx
// Server Component
import CommentForm from '@/components/CommentForm'; // Client Component
import CommentList from '@/components/CommentList'; // Server Component

async function getPost(slug: string) {
  const res = await fetch(https://api.example.com/posts/${slug});
  return res.json();
}

export default async function BlogPostPage({
  params
}: {
  params: { slug: string }
}) {
  const post = await getPost(params.slug);

  return (
    
# {post.title}
html: post.content }} /> {/ Server Component imbriqué /} {/ Client Component pour l'interactivité /} ); }

Quand utiliser Server vs Client Components

// ✅ Server Components pour:
// - Fetch de données
// - Accès direct à la base de données
// - Secrets backend (API keys)
// - Dépendances lourdes qui restent sur le serveur
// - SEO et performance

// ✅ Client Components pour:
// - Interactivité (onClick, onChange...)
// - State et effects (useState, useEffect)
// - Browser APIs (localStorage, window...)
// - Event listeners
// - Custom hooks utilisant state/effects

5. Stratégies de rendu

Static Site Generation (SSG)

// src/app/blog/[slug]/page.tsx

// Générer les pages statiques au build time
export async function generateStaticParams() {
  const posts = await fetch('https://api.example.com/posts').then(res => res.json());

  return posts.map((post: any) => ({
    slug: post.slug,
  }));
}

// Cette fonction s'exécute au build time
async function getPost(slug: string) {
  const res = await fetch(https://api.example.com/posts/${slug}, {
    next: { revalidate: false } // Cache permanent
  });
  return res.json();
}

export default async function Page({ params }: { params: { slug: string } }) {
  const post = await getPost(params.slug);

  return (
    
# {post.title}
{post.content}
); } // Metadata dynamique pour SEO export async function generateMetadata({ params }: { params: { slug: string } }) { const post = await getPost(params.slug); return { title: post.title, description: post.excerpt, }; }

Incremental Static Regeneration (ISR)

// src/app/products/page.tsx

async function getProducts() {
  const res = await fetch('https://api.example.com/products', {
    next: {
      revalidate: 3600 // Revalide toutes les heures
    }
  });
  return res.json();
}

export default async function ProductsPage() {
  const products = await getProducts();

  return (
    
# Produits {products.map((product: any) => (
## {product.name}

{product.price}€

))}
); }

Server-Side Rendering (SSR)

// src/app/dashboard/page.tsx

// Force le dynamic rendering (SSR)
export const dynamic = 'force-dynamic';
// Ou export const revalidate = 0;

async function getUserData() {
  const res = await fetch('https://api.example.com/user', {
    cache: 'no-store' // Pas de cache
  });
  return res.json();
}

export default async function DashboardPage() {
  const user = await getUserData();

  return (
    
# Bienvenue {user.name}

Dernière connexion: {new Date().toLocaleString()}

); }

Comparaison des stratégies

// SSG - Généré au build time
fetch('...', { next: { revalidate: false } })
// Avantages: Ultra rapide, CDN cacheable
// Inconvénients: Données potentiellement obsolètes

// ISR - Régénération incrémentale
fetch('...', { next: { revalidate: 60 } })
// Avantages: Équilibre performance/fraîcheur
// Inconvénients: Première requête peut être lente

// SSR - Rendu à chaque requête
fetch('...', { cache: 'no-store' })
export const dynamic = 'force-dynamic';
// Avantages: Données toujours fraîches
// Inconvénients: Plus lent, charge serveur

// On-demand Revalidation - Revalider manuellement
// Via API Route ou Server Action
revalidatePath('/blog');
revalidateTag('posts');

6. Data Fetching moderne

Fetch avec cache

// Cache persistant
const res = await fetch('https://api.example.com/data');
// Équivalent à: { cache: 'force-cache' }

// Pas de cache
const res = await fetch('https://api.example.com/data', {
  cache: 'no-store'
});

// Revalidation périodique
const res = await fetch('https://api.example.com/data', {
  next: { revalidate: 3600 }
});

// Cache avec tags (pour revalidation on-demand)
const res = await fetch('https://api.example.com/data', {
  next: { tags: ['posts'] }
});

Requêtes parallèles

// src/app/dashboard/page.tsx

async function getUser() {
  const res = await fetch('https://api.example.com/user');
  return res.json();
}

async function getPosts() {
  const res = await fetch('https://api.example.com/posts');
  return res.json();
}

async function getStats() {
  const res = await fetch('https://api.example.com/stats');
  return res.json();
}

export default async function DashboardPage() {
  // ✅ Requêtes parallèles
  const [user, posts, stats] = await Promise.all([
    getUser(),
    getPosts(),
    getStats()
  ]);

  return (
    
# {user.name}

{posts.length} posts

{stats.views} vues

); }

Streaming avec Suspense

// src/app/dashboard/page.tsx
import { Suspense } from 'react';

async function UserProfile() {
  const user = await getUser(); // Fetch lent
  return 
{user.name}
; } async function PostList() { const posts = await getPosts(); // Fetch rapide return
    {posts.map(p =>
  • {p.title}
  • )}
; } export default function DashboardPage() { return (
# Dashboard {/ Affiche immédiatement le fallback, puis le composant */} Chargement du profil...
}> Chargement des posts...
}> ); }

7. Server Actions

Action simple

// src/app/actions.ts
'use server';

import { revalidatePath } from 'next/cache';

export async function createPost(formData: FormData) {
  const title = formData.get('title') as string;
  const content = formData.get('content') as string;

  // Validation
  if (!title || !content) {
    return { error: 'Titre et contenu requis' };
  }

  // Sauvegarder en base de données
  const res = await fetch('https://api.example.com/posts', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ title, content })
  });

  if (!res.ok) {
    return { error: 'Erreur lors de la création' };
  }

  // Revalider le cache
  revalidatePath('/blog');

  return { success: true };
}

Formulaire avec Server Action

// src/app/blog/new/page.tsx
import { createPost } from '@/app/actions';

export default function NewPostPage() {
  return (
    
); }

Server Action avec état côté client

// src/components/CreatePostForm.tsx
'use client';

import { useFormState, useFormStatus } from 'react-dom';
import { createPost } from '@/app/actions';

function SubmitButton() {
  const { pending } = useFormStatus();

  return (
    
  );
}

export default function CreatePostForm() {
  const [state, formAction] = useFormState(createPost, { error: null });

  return (
    
{state?.error && (

{state.error}

)}
); }

Server Action avec optimistic UI

'use client';

import { useOptimistic } from 'react';
import { addComment } from '@/app/actions';

interface Comment {
  id: string;
  text: string;
}

export default function CommentList({ comments }: { comments: Comment[] }) {
  const [optimisticComments, addOptimisticComment] = useOptimistic(
    comments,
    (state, newComment: string) => [
      ...state,
      { id: 'temp-' + Date.now(), text: newComment }
    ]
  );

  async function handleSubmit(formData: FormData) {
    const text = formData.get('text') as string;

    // Mise à jour optimiste
    addOptimisticComment(text);

    // Server Action
    await addComment(formData);
  }

  return (
    
    {optimisticComments.map(comment => (
  • {comment.text}
  • ))}
); }

8. API Routes avec Route Handlers

GET Handler

// src/app/api/posts/route.ts
import { NextRequest, NextResponse } from 'next/server';

export async function GET(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams;
  const page = searchParams.get('page') || '1';

  const posts = await fetch(https://api.example.com/posts?page=${page})
    .then(res => res.json());

  return NextResponse.json(posts);
}

POST Handler

// src/app/api/posts/route.ts
export async function POST(request: NextRequest) {
  const body = await request.json();

  // Validation
  if (!body.title || !body.content) {
    return NextResponse.json(
      { error: 'Titre et contenu requis' },
      { status: 400 }
    );
  }

  // Créer le post
  const post = await fetch('https://api.example.com/posts', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body)
  }).then(res => res.json());

  return NextResponse.json(post, { status: 201 });
}

Route dynamique

// src/app/api/posts/[id]/route.ts
export async function GET(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  const post = await fetch(https://api.example.com/posts/${params.id})
    .then(res => res.json());

  if (!post) {
    return NextResponse.json(
      { error: 'Post introuvable' },
      { status: 404 }
    );
  }

  return NextResponse.json(post);
}

export async function DELETE(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  await fetch(https://api.example.com/posts/${params.id}, {
    method: 'DELETE'
  });

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

9. Metadata et SEO

Metadata statique

// src/app/page.tsx
import type { Metadata } from 'next';

export const metadata: Metadata = {
  title: 'Accueil',
  description: 'Page d'accueil de mon site',
  keywords: ['next.js', 'react', 'seo'],
  authors: [{ name: 'Votre Nom' }],
  openGraph: {
    title: 'Mon Site',
    description: 'Description pour les réseaux sociaux',
    images: ['/og-image.jpg'],
  },
  twitter: {
    card: 'summarylargeimage',
    title: 'Mon Site',
    description: 'Description pour Twitter',
    images: ['/twitter-image.jpg'],
  },
};

Metadata dynamique

// src/app/blog/[slug]/page.tsx
export async function generateMetadata({
  params
}: {
  params: { slug: string }
}): Promise {
  const post = await fetch(https://api.example.com/posts/${params.slug})
    .then(res => res.json());

  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      images: [post.image],
    },
  };
}

10. Tests Next.js

// tests/page.test.tsx
import { render, screen } from '@testing-library/react';
import Home from '@/app/page';

describe('Home', () => {
  it('renders homepage', () => {
    render();
    const heading = screen.getByRole('heading', { level: 1 });
    expect(heading).toBeInTheDocument();
  });
});

// tests__/api.test.ts
import { GET } from '@/app/api/posts/route';
import { NextRequest } from 'next/server';

describe('/api/posts', () => {
  it('returns posts', async () => {
    const request = new NextRequest('http://localhost:3000/api/posts');
    const response = await GET(request);
    const data = await response.json();

    expect(Array.isArray(data)).toBe(true);
  });
});

Performance Benchmarks

Temps de chargement (LCP)

Stratégie First Load Subsequent SEO
SSG 0.5s 0.2s Excellent
ISR 0.6s 0.3s Excellent
SSR 1.2s 0.8s Excellent
CSR (React) 2.5s 0.5s Moyen

Meilleures pratiques 2025

  • Server Components par défaut, Client Components seulement si nécessaire
  • ISR pour le meilleur équilibre performance/fraîcheur
  • Streaming avec Suspense pour améliorer le Time to First Byte
  • Server Actions pour les mutations sans API Routes
  • generateStaticParams pour le SSG de routes dynamiques
  • Metadata API pour le SEO optimal
  • Conclusion

    Next.js 14+ avec l’App Router offre une expérience de développement moderne et des performances exceptionnelles grâce aux Server Components et aux stratégies de rendu flexibles.

    Points clés

  • App Router pour un routing intuitif
  • Server Components pour de meilleures performances
  • SSG/ISR/SSR selon vos besoins
  • Server Actions pour simplifier les mutations
  • Metadata API pour un SEO optimal
  • Ressources

  • Next.js Documentation
  • Next.js Learn
  • Vercel Templates

Une remarque, un retour ?

Cet article est vivant — corrections, contre-arguments et retours de production sont les bienvenus. Trois canaux, choisissez celui qui vous convient.