Introduction
En 2026, Next.js domine le développement React fullstack grâce à son App Router, qui révolutionne la gestion des routes, le rendu serveur et les performances. Ce tutoriel expert vous guide dans la création d'une application de gestion de tâches scalable, intégrant Server Components, Server Actions, Streaming avec Suspense, Partial Prerendering et optimisations SEO avancées.
Pourquoi c'est crucial ? Les Server Components réduisent le JS client de 70% en moyenne, le Streaming améliore le TTFB de 50%, et le Partial Prerendering combine statique/dynamique pour des Core Web Vitals parfaits. Vous apprendrez à structurer une app prod-ready, gérer l'état serveur, sécuriser les mutations et scaler horizontalement.
Ce guide progresse des fondations à l'expertise : setup, composants hybrides, API internes, actions asynchrones, rendu conditionnel. À la fin, vous aurez une app fonctionnelle, bookmarkable pour tout lead dev. Prêt à booster vos skills Next.js ? (142 mots)
Prérequis
- Node.js 20+ (avec pnpm recommandé pour vitesse)
- Connaissances avancées en React 19, TypeScript et React Server Components
- Familiarité avec Vercel ou Docker pour déploiement
- Éditeur comme VS Code avec extensions TypeScript/Next.js
- Git pour versionning
Initialisation du projet Next.js 15+
npx create-next-app@latest todos-app --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"
cd todos-app
pnpm install @types/node
pnpm devCette commande crée un projet Next.js avec App Router activé, Tailwind pour styling rapide, ESLint pour linting strict. L'option --app force l'App Router (pas Pages). --import-alias simplifie les imports absolus. Lancez pnpm dev pour hot-reload sur http://localhost:3000. Piège : évitez yarn/npm si pnpm n'est pas installé globalement.
Structure du projet App Router
L'App Router organise le code dans src/app/. Chaque dossier est une segment de route : app/page.tsx pour /, app/todos/page.tsx pour /todos. Les layouts encapsulent (app/layout.tsx), loading.tsx gèrent Suspense, error.tsx les erreurs. Les Server Components sont par défaut (pas de '"use client"'), exécutés au serveur pour zéro JS client inutile.
Layout racine avec métadonnées SEO
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import './globals.css';
const inter = Inter({ subsets: ['latin'] });
export const metadata: Metadata = {
title: 'Todos App - Next.js Expert 2026',
description: 'App de tâches avec App Router avancé',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="fr">
<body className={inter.className}>{children}</body>
</html>
);
}Ce layout racine définit les métadonnées SEO statiques pour tout le site, utilise une font optimisée Google Fonts. Il encapsule tous les enfants sans JS client. Piège : Oubliez export const metadata, les metas dynamiques nécessitent generateMetadata. Parfait pour Lighthouse 100/100 SEO.
Page d'accueil avec Server Component
import Link from 'next/link';
export default function HomePage() {
return (
<main className="flex min-h-screen flex-col items-center justify-center p-24">
<h1 className="text-6xl font-bold mb-8">Bienvenue sur Todos App</h1>
<p className="text-xl mb-8">Gestion de tâches experte avec Next.js App Router.</p>
<Link
href="/todos"
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
>
Aller aux Todos
</Link>
</main>
);
}Server Component pur : rendu 100% serveur, zéro hydration JS. Utilise pour navigation client-side fluide. Tailwind classes inline pour rapidité. Piège : Pas d'interactivité ici (useState interdit), passez à Client Components pour ça.
Modèle de données avec Zustand-like serveur
import { unstable_cache } from 'next/cache';
type Todo = {
id: string;
text: string;
done: boolean;
};
const db = new Map<string, Todo>();
const getTodos = unstable_cache(
async (): Promise<Todo[]> => {
return Array.from(db.values());
},
['todos'],
{ revalidate: 60 }
);
export async function getAllTodos(): Promise<Todo[]> {
return getTodos();
}
export function createTodo(text: string): Todo {
const id = crypto.randomUUID();
const todo: Todo = { id, text, done: false };
db.set(id, todo);
return todo;
}
export function toggleTodo(id: string): Todo | undefined {
const todo = db.get(id);
if (todo) {
todo.done = !todo.done;
db.set(id, todo);
}
return todo;
}
export function deleteTodo(id: string): void {
db.delete(id);
}Simule une DB in-memory avec unstable_cache pour caching ISR (revalide toutes 60s). Fonctions pures pour CRUD. crypto.randomUUID() pour IDs uniques. Piège : En prod, remplacez par Prisma + PostgreSQL ; ce cache évite fetchs inutiles en dev.
Page Todos avec fetch serveur
import { getAllTodos } from '@/lib/db';
export default async function TodosPage() {
const todos = await getAllTodos();
return (
<main className="p-8 max-w-2xl mx-auto">
<h1 className="text-4xl font-bold mb-8">Mes Todos</h1>
<ul className="space-y-4">
{todos.map((todo) => (
<li key={todo.id} className="flex justify-between items-center p-4 border rounded">
<span className={todo.done ? 'line-through' : ''}>{todo.text}</span>
<span className="text-sm text-gray-500">ID: {todo.id.slice(0,8)}</span>
</li>
))}
</ul>
</main>
);
}Server Component fetch async data avec await. Données fraîches à chaque build/request. Zéro JS client téléchargé. Piège : Si erreur fetch, ajoutez error.tsx sibling. Scalable pour 100k+ items via pagination.
Server Actions pour mutations sécurisées
Les Server Actions remplacent les API routes pour mutations : exécutées serveur, revalident cache automatiquement via revalidatePath. Sécurisées (CSRF auto), pas d'exposition publique. Idéales pour forms sans fetch client.
Server Action et Form Client Component
'use server';
import { revalidatePath } from 'next/cache';
import { createTodo, toggleTodo, deleteTodo } from '@/lib/db';
export async function addTodo(formData: FormData) {
'use server';
const text = formData.get('text') as string;
createTodo(text);
revalidatePath('/todos');
}
export async function toggle(formData: FormData) {
'use server';
const id = formData.get('id') as string;
toggleTodo(id!);
revalidatePath('/todos');
}
export async function removeTodo(formData: FormData) {
'use server';
const id = formData.get('id') as string;
deleteTodo(id!);
revalidatePath('/todos');
}'use server' au top pour Action serveur. FormData pour progressive enhancement (JS off = OK). revalidatePath update UI sans full reload. Piège : Toujours valider/sanitzer formData en prod (zod recommandé).
Page Todos interactive avec Actions
import { Suspense } from 'react';
import { getAllTodos } from '@/lib/db';
import TodoList from './TodoList';
import { addTodo } from './actions';
export default async function TodosPage() {
const todos = await getAllTodos();
return (
<main className="p-8 max-w-2xl mx-auto">
<h1 className="text-4xl font-bold mb-8">Mes Todos</h1>
<form action={addTodo} className="mb-8">
<input
name="text"
type="text"
placeholder="Nouvelle tâche..."
className="border p-2 mr-2 w-64"
required
/>
<button type="submit" className="bg-green-500 text-white px-4 py-2 rounded">
Ajouter
</button>
</form>
<Suspense fallback={<p>Chargement...</p>}>
<TodoList initialTodos={todos} />
</Suspense>
</main>
);
}Hybride : Server fetch initial + Client pour interactivité via Actions. stream le contenu. Props initialTodos hydratent le client. Piège : Server Actions ne leakent pas data ; toujours async.
Client Component TodoList avec useActionState
'use client';
import { useActionState } from 'react';
import { toggle, removeTodo } from './actions';
type Todo = { id: string; text: string; done: boolean };
interface Props {
initialTodos: Todo[];
}
export default function TodoList({ initialTodos }: Props) {
const [state, toggleAction, isPending] = useActionState(toggle, undefined);
const [stateDel, delAction, isPendingDel] = useActionState(removeTodo, undefined);
return (
<ul className="space-y-4">
{initialTodos.map((todo) => (
<li key={todo.id} className="flex justify-between items-center p-4 border rounded">
<span className={todo.done ? 'line-through' : ''}>{todo.text}</span>
<div>
<form action={toggleAction}>
<input type="hidden" name="id" value={todo.id} />
<button
type="submit"
disabled={isPending}
className="bg-blue-500 text-white px-3 py-1 rounded mr-2"
>
{isPending ? '...' : 'Toggle'}
</button>
</form>
<form action={delAction}>
<input type="hidden" name="id" value={todo.id} />
<button
type="submit"
disabled={isPendingDel}
className="bg-red-500 text-white px-3 py-1 rounded"
>
Supprimer
</button>
</form>
</div>
</li>
))}
</ul>
);
}'use client' pour hooks React 19. useActionState gère pending states natif, remplace useTransition. Optimisé : un seul re-render par action. Piège : initialTodos statique ; pour live updates, ajoutez useSWR ou WebSockets.
Route Handler API pour intégrations externes
import { NextRequest, NextResponse } from 'next/server';
import { getAllTodos, createTodo } from '@/lib/db';
export async function GET() {
const todos = await getAllTodos();
return NextResponse.json(todos);
}
export async function POST(request: NextRequest) {
const { text } = await request.json();
if (!text || text.length < 3) {
return NextResponse.json({ error: 'Texte trop court' }, { status: 400 });
}
const todo = createTodo(text);
return NextResponse.json(todo, { status: 201 });
}Route Handler pour API RESTful. GET/POST standards, validation input. Partage DB avec app. Piège : Pas de CORS par défaut ; ajoutez middleware pour prod. Idéal pour mobile apps ou tiers.
Streaming et Partial Prerendering
Streaming avec Suspense divise le rendu : shell statique immédiat, contenu async streamé. Partial Prerendering (Next 15+) prerendert statique + dynamique on-demand : dynamic = 'force-dynamic' ou dynamicParams. Boost LCP de 40%.
Exemple Streaming avec loading.tsx
export default function Loading() {
return (
<main className="p-8 max-w-2xl mx-auto">
<div className="animate-pulse space-y-4">
<div className="h-8 bg-gray-200 rounded w-48 mb-8"></div>
<div className="h-4 bg-gray-200 rounded w-full mb-2"></div>
<div className="h-4 bg-gray-200 rounded w-3/4"></div>
</div>
</main>
);
}loading.tsx auto-injecté par Suspense. Skeleton Tailwind pour UX fluide. Streamé après shell. Piège : Pas de data dans loading ; gardez-le pure UI.
Bonnes pratiques
- Toujours Server Components first : minimisez 'use client' à 10% du code.
- Cachings stratèges : unstable_cache + revalidatePath/Tag pour data fraîche.
- Zod pour validation : schemas sur actions/forms (ex: z.string().min(3)).
- Middleware auth : Protégez routes sensibles (/todos -> auth check).
- Observabilité : Intégrez Sentry + Vercel Analytics pour erreurs/perf.
Erreurs courantes à éviter
- Hydration mismatch : Server/Client data diff (utilisez initialProps).
- Server Actions sans 'use server' : Erreur runtime (top du fichier).
- Oubli Suspense : Blocage rendu (wrap async components).
- Cache infini : Ajoutez revalidate ou dynamic='force-dynamic' pour user data.
Pour aller plus loin
Maîtrisez Next.js 15+ en profondeur avec nos formations Learni expertes. Ressources : Docs App Router, Vercel Templates, React 19 Server Components. Déployez sur Vercel pour Edge Runtime gratuit.