Aller au contenu

Next.js : données et routes API

export default async function Produits() {
const reponse = await fetch('https://api.exemple.com/produits');
if (!reponse.ok) throw new Error('Chargement impossible');
const produits: { id: number; nom: string }[] = await reponse.json();
return (
<ul>
{produits.map((p) => (
<li key={p.id}>{p.nom}</li>
))}
</ul>
);
}

throw déclenche la page error.tsx la plus proche (voir Routage avec l’App Router).

La politique de cache de fetch a changé selon les versions de Next.js : depuis Next.js 15, les requêtes fetch ne sont plus mises en cache par défaut. Choisir explicitement :

await fetch(url, { cache: 'force-cache' }); // mis en cache
await fetch(url, { next: { revalidate: 3600 } }); // mis en cache, rafraîchi toutes les heures
await fetch(url, { next: { tags: ['produits'] } }); // étiqueté pour une revalidation ciblée
await fetch(url, { cache: 'no-store' }); // jamais mis en cache

Revalider à la demande, après une écriture :

'use server';
import { revalidatePath, revalidateTag } from 'next/cache';
export async function apresModification() {
revalidateTag('produits');
revalidatePath('/produits');
}

Les pages sans donnée dépendant de la requête sont statiques (construites au build) ; lire cookies(), headers() ou searchParams les rend dynamiques. La sortie de npm run build indique, pour chaque route, ○ (statique) ou ƒ (dynamique).

Les règles de cache évoluent d’une version à l’autre : vérifier le comportement avec la version installée (npm ls next).

app/api/clients/route.ts :

import { NextResponse } from 'next/server';
import { z } from 'zod';
const schema = z.object({
nom: z.string().min(1).max(100),
email: z.string().email(),
});
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const page = Math.max(1, Number(searchParams.get('page')) || 1);
const clients = await listerClients(page);
return NextResponse.json({ page, donnees: clients });
}
export async function POST(request: Request) {
const corps = await request.json().catch(() => null);
const resultat = schema.safeParse(corps);
if (!resultat.success) {
return NextResponse.json({ erreur: 'données invalides' }, { status: 400 });
}
const client = await insererClient(resultat.data);
return NextResponse.json(client, { status: 201 });
}

Route dynamique : app/api/clients/[id]/route.ts :

export async function GET(
_request: Request,
{ params }: { params: Promise<{ id: string }> },
) {
const { id } = await params;
const client = await trouverClient(Number(id));
if (!client)
return NextResponse.json({ erreur: 'introuvable' }, { status: 404 });
return NextResponse.json(client);
}

Les fonctions portent le nom de la méthode HTTP (GET, POST, PUT, PATCH, DELETE).

Fenêtre de terminal
curl -s http://127.0.0.1:3000/api/clients | jq .
curl -s -X POST http://127.0.0.1:3000/api/clients -H 'Content-Type: application/json' \
-d '{"nom":"Alice","email":"alice@exemple.com"}'

Dans un composant serveur, un route handler ou une Server Action, accéder directement à la base :

lib/db.ts
import 'server-only';
import { Pool } from 'pg';
const globale = globalThis as unknown as { pool?: Pool };
export const pool =
globale.pool ??
new Pool({ connectionString: process.env.DATABASE_URL, max: 10 });
if (process.env.NODE_ENV !== 'production') globale.pool = pool;

La mise en cache de globalThis évite de créer un nouveau pool à chaque rechargement en développement. Les requêtes sont toujours paramétrées ($1) : voir Express : API JSON. Pour MongoDB avec Mongoose : Mongoose : installation et connexion.

Un fichier middleware.ts à la racine (ou src/) s’exécute avant les requêtes :

import { NextResponse, type NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const session = request.cookies.get('session')?.value;
if (!session && request.nextUrl.pathname.startsWith('/admin')) {
return NextResponse.redirect(new URL('/connexion', request.url));
}
return NextResponse.next();
}
export const config = { matcher: ['/admin/:path*'] };

Dans les versions récentes de Next.js, ce fichier peut s’appeler proxy.ts : lire le guide de la version utilisée.

Pour l’authentification, utiliser une bibliothèque éprouvée (Auth.js, Better Auth…) plutôt que d’écrire la gestion des sessions et des mots de passe soi-même.

'use client';
import { useEffect, useState } from 'react';
export function ListeClients() {
const [clients, setClients] = useState<{ id: number; nom: string }[]>([]);
useEffect(() => {
fetch('/api/clients')
.then((r) => r.json())
.then((d) => setClients(d.donnees));
}, []);
return (
<ul>
{clients.map((c) => (
<li key={c.id}>{c.nom}</li>
))}
</ul>
);
}

Pour les données serveur à afficher, préférer un composant serveur : pas de requête supplémentaire depuis le navigateur.

Symptôme Piste
Données périmées Cache : cache: 'no-store', revalidate, revalidatePath
Données jamais rafraîchies en production Page statique construite au build : rendre dynamique ou revalider
Dynamic server usage au build Lecture de cookies()/headers() dans une page statique : export const dynamic = 'force-dynamic'
ECONNREFUSED à la base au build Accès à la base pendant la construction : ne pas interroger la base dans une page statique ou rendre la page dynamique
params est une promesse await params
Trop de connexions à la base en développement Pool mis en cache sur globalThis
405 Method Not Allowed Fonction de la méthode HTTP non exportée

Ubuntu 24.04 et 26.04 : différences à connaître

Section intitulée « Ubuntu 24.04 et 26.04 : différences à connaître »

Aucune.