Chapitre 11.8 — ⭐ Consommer l’API BookLab dans Next.js
Partie 11 : API Platform — Chapitre 8/8 Version de référence : Symfony 7.4 (API) / Next.js 15 (App Router).
⏱️ TL;DR
- Architecture recommandée : le serveur Next.js parle à l’API Symfony (server-to-server) ; le navigateur ne voit que Next.js. Le JWT vit dans un cookie httpOnly, jamais exposé au JS client.
- Login : une Server Action (ou Route Handler) appelle
POST /api/login_check, reçoit le JWT, et le pose en cookie httpOnly.- Lecture : dans les Server Components (RSC), on lit le cookie et on
fetchl’API côté serveur — avec le cache et la revalidation de Next.- Écriture : des Server Actions appellent l’API (POST/PATCH/DELETE) puis
revalidateTag/revalidatePathpour rafraîchir les données.- Types : on génère un client TypeScript typé depuis la spec OpenAPI (
openapi-typescript) → type-safety de bout en bout.- Erreurs : 401 → refresh/redirection login ; 422 → erreurs de champ ; le tout mappé une fois.
🎯 Objectifs
- Mettre en place l’auth JWT côté Next.js (cookie httpOnly, Server Action).
- Lire l’API depuis des Server Components avec cache/revalidation.
- Écrire via des Server Actions et rafraîchir les données.
- Générer des types TS depuis OpenAPI et gérer les erreurs.
1. L’architecture : Next.js comme façade
Le navigateur ne parle qu’à Next.js. Next.js relaie vers Symfony côté serveur, en joignant le JWT (lu depuis le cookie httpOnly). Bénéfices : pas de CORS en prod (même origine), JWT jamais exposé au JS client (anti-XSS), et le rendu serveur (RSC) reste rapide.
Configuration de base — l’URL de l’API :
# .env.local (Next.js)
API_URL=http://localhost:80002. Le login : Server Action + cookie httpOnly
// app/login/actions.ts
'use server';
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';
export async function login(formData: FormData) {
const res = await fetch(`${process.env.API_URL}/api/login_check`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: formData.get('email'),
password: formData.get('password'),
}),
});
if (!res.ok) {
return { error: 'Identifiants invalides.' }; // 401
}
const { token, refresh_token } = await res.json();
// JWT en cookie httpOnly : inaccessible au JS client (anti-XSS)
const store = await cookies();
store.set('jwt', token, { httpOnly: true, secure: true, sameSite: 'lax', path: '/' });
store.set('refresh', refresh_token, { httpOnly: true, secure: true, sameSite: 'lax', path: '/' });
redirect('/dashboard');
}Le mot de passe est vérifié par Symfony (haché, Partie 10.3) ; le JWT revient et est scellé dans un cookie httpOnly par le serveur Next. Le navigateur ne manipule jamais le token en clair.
3. Un helper de fetch authentifié
// app/lib/api.ts
import { cookies } from 'next/headers';
export async function apiFetch(path: string, init: RequestInit = {}) {
const token = (await cookies()).get('jwt')?.value;
const res = await fetch(`${process.env.API_URL}${path}`, {
...init,
headers: {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
...init.headers,
},
});
if (res.status === 401) {
// token expiré → tenter un refresh (Partie 10.4), sinon rediriger vers /login
}
return res;
}4. Lire l’API depuis un Server Component (RSC)
Dans l’App Router, un Server Component peut fetch directement — c’est du code serveur, le JWT ne transite jamais par le navigateur.
// app/dashboard/bookings/page.tsx (Server Component)
import { apiFetch } from '@/app/lib/api';
export default async function BookingsPage() {
const res = await apiFetch('/api/bookings?order[start]=desc', {
// cache Next : revalider toutes les 60s, taggué pour invalidation ciblée
next: { revalidate: 60, tags: ['bookings'] },
headers: { Accept: 'application/json' }, // JSON pur plutôt que JSON-LD
});
const bookings = await res.json();
return (
<ul>
{bookings.map((b: Booking) => (
<li key={b.id}>{b.customerEmail} — {b.statusLabel}</li>
))}
</ul>
);
}Le cache de fetch de Next.js (next: { revalidate, tags }) met en cache la réponse et la revalide selon la stratégie — combiné au cache HTTP de Symfony (Partie 15), c’est très performant.
5. Écrire via une Server Action + revalidation
// app/dashboard/bookings/actions.ts
'use server';
import { apiFetch } from '@/app/lib/api';
import { revalidateTag } from 'next/cache';
export async function createBooking(formData: FormData) {
const res = await apiFetch('/api/bookings', {
method: 'POST',
body: JSON.stringify({
customerEmail: formData.get('email'),
start: formData.get('start'),
}),
});
if (res.status === 422) {
const problem = await res.json();
return { errors: problem.violations }; // → afficher sous chaque champ
}
if (!res.ok) {
return { error: 'Erreur serveur.' };
}
revalidateTag('bookings'); // invalide le cache de la liste → RSC re-fetch
return { success: true };
}Le formulaire côté client appelle cette Server Action. Après succès, revalidateTag('bookings') invalide le cache de la liste : le Server Component qui affiche les réservations se re-rend avec les données fraîches. C’est le cycle mutation → revalidation de l’App Router, branché sur l’API Symfony.
6. Types de bout en bout depuis OpenAPI
On génère les types TypeScript depuis la spec OpenAPI de l’API (chapitre 11.6) — plus besoin d’écrire les types à la main :
# côté projet Next.js
npx openapi-typescript http://localhost:8000/api/docs.json -o src/lib/api-types.tsOn obtient des types Booking, Provider… exactement alignés sur l’API (groupes de sérialisation inclus). Le front devient type-safe : si l’API change un champ, tsc le signale. C’est l’un des plus grands atouts du couple API Platform + Next.js : le contrat (OpenAPI) génère les types du client.
7. Gérer l’authentification et les erreurs, une fois
| Réponse | Traitement côté Next.js |
|---|---|
| 401 | tenter POST /api/token/refresh (refresh cookie) ; sinon rediriger vers /login |
| 403 | afficher « accès refusé » / cacher l’action |
| 422 | mapper violations[].propertyPath sur les champs du formulaire |
| 404 | notFound() (page 404 Next) |
| 5xx | page d’erreur / réessai |
Comme le format est standard (RFC 7807, Partie 11.4), ce mapping se code une seule fois dans apiFetch (ou une couche au-dessus) et sert partout.
⚠️ Piège : ne stockez jamais le JWT dans
localStorageou un cookie non-httpOnly côté client « pour y accéder en JS ». C’est la porte au vol de token par XSS. Le pattern sûr : JWT en cookie httpOnly, posé et lu côté serveur Next (Server Actions/RSC/Route Handlers). Le client ne voit jamais le token — il déclenche juste des actions serveur.
💡 Pour un dev Next.js : vous reconnaissez tout — Server Actions,
cookies(),revalidateTag, RSC. La seule « nouveauté » est que la source de données est votre API Symfony (au lieu d’un ORM direct dans Next). Vous gardez vos réflexes App Router ; Symfony fournit une API robuste, validée, sécurisée, documentée. C’est exactement le profil qui se vend cher (Partie 17) : back Symfony solide + front Next.js moderne, reliés proprement.
8. Récapitulatif : le pont complet
Vous avez construit le cœur premium du cours : une API BookLab complète et un front Next.js qui la consomme proprement. Reste à la rendre temps réel (Partie 12).
✏️ Exercices
Exercice 1. Écrivez la Server Action de connexion qui pose le JWT en cookie httpOnly.
✅ Solution
Voir le §2 : fetch('/api/login_check') avec email/mot de passe, puis cookies().set('jwt', token, { httpOnly: true, secure: true, sameSite: 'lax' }), et redirect('/dashboard'). En cas de 401, renvoyer { error }. Le point clé : le cookie est httpOnly (posé côté serveur), donc le JWT n’est jamais accessible au JS du navigateur.
Exercice 2. Après une création réussie via Server Action, la liste (RSC) ne se met pas à jour. Que manque-t-il ?
✅ Solution
L’invalidation du cache : revalidateTag('bookings') (ou revalidatePath('/dashboard/bookings')) après le POST réussi. Sans elle, le Server Component qui affiche la liste sert la version cachée (next: { tags: ['bookings'] }). revalidateTag marque ce cache périmé → la liste se re-fetch au prochain rendu.
Exercice 3. Pourquoi lire l’API dans un Server Component est-il préférable à un fetch côté client pour BookLab ?
✅ Solution
Parce que le fetch est côté serveur : le JWT (cookie httpOnly) n’est jamais exposé au navigateur (anti-XSS), on bénéficie du cache/revalidation de Next, du SSR/SEO, et on évite le CORS (server-to-server). Un fetch client exposerait le token et imposerait le CORS. On réserve le fetch client aux interactions qui l’exigent vraiment (et on passe alors par un Route Handler Next qui relaie, gardant le token côté serveur).
🧠 Quiz de révision
1. Où stocke-t-on le JWT côté Next.js, et pourquoi ?
Dans un cookie httpOnly (+ Secure, SameSite), posé côté serveur (Server Action) : inaccessible au JS client → protégé du XSS.
2. Comment lit-on l’API dans un Server Component ?
Avec fetch (via un helper qui joint le Bearer depuis le cookie), en passant next: { revalidate, tags } pour le cache/revalidation. Code serveur, token non exposé.
3. Comment rafraîchit-on les données après une mutation ?
Avec revalidateTag('...') (ou revalidatePath) dans la Server Action après l’écriture réussie, ce qui re-fetch les RSC concernés.
4. Comment obtient-on des types TypeScript alignés sur l’API ?
En générant les types depuis la spec OpenAPI (openapi-typescript http://.../api/docs.json) → type-safety de bout en bout.
5. Que fait le front sur un 422 renvoyé par l’API ?
Il lit violations[].propertyPath/message (RFC 7807) et affiche les erreurs sous chaque champ du formulaire.
Fin de la Partie 11 — le cœur premium du cours. BookLab est une API complète (REST + GraphQL en option), sérialisée, validée, sécurisée et documentée, consommée par un front Next.js propre et type-safe. Il ne manque plus que le temps réel.
Prochaine étape : Partie 12 — Temps réel & asynchrone — Mercure, Messenger, Scheduler, Mailer et Stripe.