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 (
<main>
# Bienvenue
<p>Page d'accueil</p>
</main>
);
}
// src/app/about/page.tsx - Route: /about
export default function AboutPage() {
return (
<div>
# À propos
</div>
);
}
// src/app/blog/page.tsx - Route: /blog
export default function BlogPage() {
return (
<div>
# Blog
</div>
);
}
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>
# Article: {params.slug}
<p>Query params: {JSON.stringify(searchParams)}</p>
</article>
);
}
// 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 (
<div>
# Catégorie: {params.category}
## Produit: {params.product}
</div>
);
}
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 (
<div>
# Documentation
<p>Path: {params.slug.join('/')}</p>
</div>
);
}
// 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 (
<html lang="fr">
<body className={inter.className}>
<header>
<nav>Navigation globale</nav>
</header>
<main>{children}</main>
<footer>Footer global</footer>
</body>
</html>
);
}
Layouts imbriqués
// src/app/dashboard/layout.tsx
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="dashboard">
<aside className="sidebar">
<nav>
<a href="/dashboard">Accueil</a>
<a href="/dashboard/settings">Paramètres</a>
<a href="/dashboard/profile">Profil</a>
</nav>
</aside>
<div className="content">
{children}
</div>
</div>
);
}
// 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 (
<div className="template-wrapper">
<div className="animation-enter">
{children}
</div>
</div>
);
}
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 (
<div>
# Blog
{posts.map((post: any) => (
<article key={post.id}>
## {post.title}
<p>{post.excerpt}</p>
</article>
))}
</div>
);
}
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 (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}
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 (
<article>
# {post.title}
<div dangerouslySetInnerHTML={{ **html: post.content }} />
{/* Server Component imbriqué */}
<CommentList postId={post.id} />
{/* Client Component pour l'interactivité */}
<CommentForm postId={post.id} />
</article>
);
}
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 (
<article>
# {post.title}
<div>{post.content}</div>
</article>
);
}
// 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 (
<div>
# Produits
{products.map((product: any) => (
<div key={product.id}>
## {product.name}
<p>{product.price}€</p>
</div>
))}
</div>
);
}
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 (
<div>
# Bienvenue {user.name}
<p>Dernière connexion: {new Date().toLocaleString()}</p>
</div>
);
}
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 (
<div>
# {user.name}
<p>{posts.length} posts</p>
<p>{stats.views} vues</p>
</div>
);
}
Streaming avec Suspense
// src/app/dashboard/page.tsx
import { Suspense } from 'react';
async function UserProfile() {
const user = await getUser(); // Fetch lent
return <div>{user.name}</div>;
}
async function PostList() {
const posts = await getPosts(); // Fetch rapide
return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
}
export default function DashboardPage() {
return (
<div>
# Dashboard
{/* Affiche immédiatement le fallback, puis le composant */}
<Suspense fallback={<div>Chargement du profil...</div>}>
<UserProfile />
</Suspense>
<Suspense fallback={<div>Chargement des posts...</div>}>
<PostList />
</Suspense>
</div>
);
}
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 (
<form action={createPost}>
<input
type="text"
name="title"
placeholder="Titre"
required
/>
<textarea
name="content"
placeholder="Contenu"
required
/>
<button type="submit">Publier</button>
</form>
);
}
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 (
<button type="submit" disabled={pending}>
{pending ? 'Publication...' : 'Publier'}
</button>
);
}
export default function CreatePostForm() {
const [state, formAction] = useFormState(createPost, { error: null });
return (
<form action={formAction}>
<input type="text" name="title" required />
<textarea name="content" required />
{state?.error && (
<p className="error">{state.error}</p>
)}
<SubmitButton />
</form>
);
}
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 (
<div>
<ul>
{optimisticComments.map(comment => (
<li key={comment.id}>{comment.text}</li>
))}
</ul>
<form action={handleSubmit}>
<input type="text" name="text" required />
<button type="submit">Commenter</button>
</form>
</div>
);
}
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: 'summary_large_image',
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<Metadata> {
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(<Home />);
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
- 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
- Next.js Documentation
- Next.js Learn
- Vercel Templates
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.