Next.js (App Router)
Démarrer
npx create-next-app@latest mon-app --ts --tailwind --app --no-src-dir
cd mon-app && npm run dev
▲ Next.js 16.3.0 (Turbopack)
- Local: http://localhost:3000
✓ Ready in 812ms
Routage par fichiers
app/
layout.tsx → enveloppe toutes les pages
page.tsx → /
globals.css
blog/
page.tsx → /blog
[slug]/page.tsx → /blog/:slug
api/
items/route.ts → /api/items
(marketing)/ → groupe, n'apparaît pas dans l'URL
loading.tsx → état de chargement automatique
error.tsx → frontière d'erreur (doit être "use client")
not-found.tsx → 404
Server Components par défaut
Tout composant est serveur sauf mention contraire. Il peut être async, lire le
système de fichiers, appeler une base — et son code n'est jamais envoyé au navigateur.
// app/items/page.tsx — server component
import { readFileSync } from "node:fs";
export default async function Items() {
const data = JSON.parse(readFileSync("content/items.json", "utf8"));
return <ul>{data.map((i) => <li key={i.id}>{i.title}</li>)}</ul>;
}
"use client" en première ligne bascule un fichier côté navigateur. Nécessaire dès qu'on
utilise useState, useEffect, un gestionnaire d'événement ou une API du DOM.
"use client";
import { useState } from "react";
export function Counter() {
const [n, setN] = useState(0);
return <button onClick={() => setN(n + 1)}>{n}</button>;
}
Règle de composition : un composant serveur peut importer un composant client, l'inverse
est impossible. Donc on descend "use client" le plus bas possible dans l'arbre.
Params et search params sont asynchrones
export default async function Page({
params,
searchParams,
}: {
params: Promise<{ slug: string }>;
searchParams: Promise<{ tag?: string }>;
}) {
const { slug } = await params;
const { tag } = await searchParams;
return <h1>{slug} {tag}</h1>;
}
C'est le changement qui casse le plus de code venu des anciennes versions : params était
un objet simple, il faut maintenant l'attendre.
Génération statique
export function generateStaticParams() {
return getAllSlugs().map((slug) => ({ slug }));
}
export async function generateMetadata({ params }): Promise<Metadata> {
const { slug } = await params;
return { title: `${slug} — Mon site` };
}
Avec generateStaticParams, chaque route est prérendue au build. Sans, elle est rendue à
la demande.
Routes API
// app/api/items/route.ts
import { NextResponse } from "next/server";
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
return NextResponse.json({ q: searchParams.get("q") });
}
export async function POST(request: Request) {
const body = await request.json();
return NextResponse.json({ ok: true, body }, { status: 201 });
}
Server Actions
// app/actions.ts
"use server";
export async function createItem(formData: FormData) {
const title = formData.get("title") as string;
await db.insert({ title });
revalidatePath("/items");
}
import { createItem } from "./actions";
export default function Form() {
return (
<form action={createItem}>
<input name="title" />
<button type="submit">Créer</button>
</form>
);
}
Une mutation serveur appelée depuis le client sans écrire de route API.
Navigation et images
import Link from "next/link";
import Image from "next/image";
<Link href="/blog/hello" prefetch>Article</Link>
<Image src="/photo.jpg" alt="" width={800} height={600} priority />
"use client";
import { useRouter, usePathname, useSearchParams } from "next/navigation";
const router = useRouter();
router.push("/blog");
router.refresh(); // recharge les données serveur sans perdre l'état client
next/navigation, pas next/router — ce dernier appartient au Pages Router.
Variables d'environnement
DATABASE_URL=postgres://... # serveur uniquement
NEXT_PUBLIC_API_URL=https://... # exposée au navigateur
Seul le préfixe NEXT_PUBLIC_ traverse vers le client. Tout le reste reste serveur — c'est
la protection à ne pas contourner par confort.
Build et déploiement
npm run build # vérifie les types et prérend
npm start # sert le build de production
npx vercel --prod # déploie
Route (app)
┌ ○ / 142 B 102 kB
├ ● /blog/[slug] 1.2 kB 118 kB
└ ƒ /api/items 0 B 0 B
○ Static ● SSG ƒ Dynamic
Lire cette table à chaque build : une route passée en ƒ alors qu'elle devrait être
statique signale un appel dynamique involontaire — cookies(), headers() ou un fetch
non caché.