Next.js : front-end, back-end ou les deux ?
Next.js sait exécuter du code serveur, mais ne remplace pas toujours une API. À partir de quatre projets aux choix différents, d’un portfolio sans serveur à une plateforme en microservices, voici ce que Next.js fait bien côté serveur, les critères qui imposent une API séparée et les pièges à éviter entre les deux.
« Next.js, c’est du front ou du back ? » La question revient souvent, parce que Next.js fait un peu des deux. Il rend des pages côté serveur, reçoit des formulaires et peut exposer des points d’API, si bien qu’on se demande s’il faut encore un back-end à part. J’ai travaillé sur quatre projets qui y ont répondu différemment. Ce portfolio n’exécute aucun code serveur au moment des visites, mon socle Payload tient en une seule application Next.js, l’e-commerce Cora s’appuyait sur l’API d’un prestataire et la DXP Goodbizz sur des microservices Express. Chaque choix avait ses raisons, et ce sont elles qui permettent de trancher sur un nouveau projet.
Que sait faire Next.js côté serveur ?
Avec l’App Router, Next.js propose trois façons d’exécuter du code sur le serveur, chacune avec un rôle précis :
- les Server Components lisent les données directement à leur source (base de données, CMS ou API) pendant le rendu de la page ;
- les Server Actions traitent les modifications envoyées par l’interface, comme l’envoi d’un formulaire, sans écrire de route ;
- les Route Handlers exposent des points d’API publics, qui répondent à n’importe quelle requête HTTP et peuvent renvoyer du JSON, un flux RSS ou un fichier.
La documentation de Next.js le dit elle-même, ces capacités « ne remplacent pas un back-end complet ». Elles forment une couche d’API au service de l’interface, ce qu’on appelle un backend for frontend. Reste à savoir, pour chaque projet, si son code serveur doit vivre dans l’application qui affiche les pages ou à côté.
Quand Next.js suffit-il comme back-end ?
Ce portfolio n’exécute même aucun code serveur au moment des visites. Son contenu est écrit dans le dépôt, et toutes ses pages sont générées au build, comme l’explique l’article sur les modes de rendu. Le seul besoin serveur, l’envoi du formulaire de contact, passe par EmailJS, un service tiers appelé depuis le navigateur. Pour un site qui n’a aucune donnée à enregistrer, un service spécialisé demande moins d’entretien qu’une API à héberger, en échange d’une dépendance à ce service et à ses limites d’envoi.
Le socle Payload va plus loin, avec une base PostgreSQL, une administration et des comptes. Payload s’installe pourtant dans l’application Next.js elle-même. Les pages lisent leurs données par la Local API, sans requête HTTP, et les formulaires passent par une Server Action. L’API REST de Payload existe aussi, exposée par un Route Handler généré, pour les appels qui viennent de l’extérieur :
// Simplified example: a Server Action writes through Payload's Local API
'use server'
import { headers } from 'next/headers'
import { getPayload } from 'payload'
import config from '@payload-config'
export async function submitForm(formId: number, entries: FormEntry[]) {
const payload = await getPayload({ config })
await payload.create({
collection: 'form-submissions',
overrideAccess: false, // apply the collection's access rules to this anonymous visitor
req: { headers: await headers() }, // the visitor's real headers, used by rate limiting
data: { form: formId, submissionData: entries },
})
return { success: true }
}Passer par la Local API suit une recommandation de la documentation. Un composant serveur ou une action lit la donnée à sa source, plutôt qu’en appelant les Route Handlers de sa propre application, ce qui ajouterait un aller-retour HTTP et ferait même échouer le build d’une page pré-rendue. Un détail m’a pourtant surpris. Sans en-têtes transmis, la Local API en fabrique de vides, si bien que la limitation des envois, fondée sur l’adresse IP, voyait tous les visiteurs comme un seul. Transmettre les en-têtes de la requête réelle règle le problème.
Dans ces deux cas, l’application est le seul client de ses données, les traitements sont courts et une seule équipe fait évoluer l’ensemble. Une seule application à développer, à tester et à déployer reste alors le choix le plus simple.
Quand faut-il une API séparée ?
Les deux autres projets ont fait le choix inverse, et leurs raisons se retrouvent dans la plupart des cas où une API séparée s’impose :
- plusieurs clients consomment les mêmes données. Chez Cora, l’API servait déjà les applications iOS et Android avant le nouveau site web, et chez Goodbizz, les mêmes services alimentent des sites, des applications, des e-mails et un back-office React. Une API rangée dans le site obligerait tous ces clients à dépendre de lui ;
- une autre équipe possède l’API. Celle de Cora était maintenue par un prestataire externe, et l’équipe web ne pouvait pas la modifier ;
- des connexions restent ouvertes. Une messagerie en WebSocket, comme celle de SRS, demande un serveur qui garde chaque connexion vivante. Hébergé en fonctions serverless, un Route Handler s’arrête à la fin de sa réponse ou au bout d’un délai, et la documentation de Next.js précise que les WebSockets n’y fonctionnent pas ;
- des traitements durent. Analyser une vidéo ou calculer les empreintes de toute une médiathèque prend plus de temps qu’une requête ne devrait en prendre, et se range mieux dans un service dédié.
Les microservices de Goodbizz poussent cette logique jusqu’au bout, avec une API gateway devant des services spécialisés. Le front Next.js n’y est qu’un client parmi d’autres.
Comment brancher Next.js sur une API existante ?
Sur Cora, le front Next.js appelait l’API depuis le serveur, dans des fonctions que les pages et le layout attendaient avant d’envoyer le HTML. L’adresse de l’API et ses clés restaient dans des variables d’environnement lues côté serveur uniquement. Seuls deux Route Handlers existaient, pour l’aperçu des contenus Prismic et pour le webhook qui invalide le cache à chaque publication.
Ce découpage repose sur trois règles. L’API s’appelle depuis le serveur dès que possible, pour garder ses secrets hors du navigateur. Aucun secret ne porte le préfixe NEXT_PUBLIC_, car Next.js inscrit la valeur de ces variables dans le JavaScript envoyé au navigateur, où n’importe qui peut la lire. Enfin, un module qui détient des secrets se marque avec import 'server-only', pour que son import par erreur dans un composant client fasse échouer le build :
// lib/api.ts (simplified): this module can only run on the server
import 'server-only'
const API_URL = process.env.API_URL // no NEXT_PUBLIC_ prefix: never sent to the browser
const API_KEY = process.env.API_KEY
export async function getStores() {
const res = await fetch(`${API_URL}/stores`, { headers: { 'x-api-key': API_KEY } })
if (!res.ok) throw new Error(`Stores request failed: ${res.status}`)
return res.json()
}La session de l’utilisateur suit le même chemin. Stockée dans un cookie httpOnly, elle est lue côté serveur et transmise à l’API, sans passer par le JavaScript du navigateur. Quand l’API exige un token de session qui expire, son renouvellement demande aussi un peu de méthode, détaillée dans le retour d’expérience sur Cora.
Quels pièges éviter entre les deux ?
- Lire des données avec une Server Action. Elles sont faites pour les modifications, et Next.js les exécute les unes après les autres, si bien que plusieurs lectures lancées ainsi s’enchaînent au lieu de partir en parallèle. Un Server Component lit la donnée directement.
- Garder un état dans une variable de module côté serveur. Sur un serveur Node.js, un module est chargé une fois et partagé par toutes les requêtes. Une file d’attente ou un indicateur de renouvellement en cours déclaré à ce niveau devient commun à tous les visiteurs, et l’un peut recevoir le résultat prévu pour un autre. L’état propre à un visiteur vit dans sa session ou dans la requête.
- Laisser un webhook ouvert. Un Route Handler qui invalide le cache ou déclenche un traitement est public. Il vérifie un secret ou une signature avant d’agir, faute de quoi n’importe qui peut le déclencher.
- Dupliquer les règles. Quand Next.js et l’API valident chacun les mêmes données ou vérifient chacun les droits, les deux versions finissent par diverger. L’API reste la référence, et Next.js peut valider pour guider l’utilisateur, jamais à sa place.
- Oublier les limites de l’hébergement. Sur une plateforme serverless, chaque Route Handler peut tourner dans une fonction isolée, sans mémoire partagée entre les requêtes, sans écriture disque garantie et avec une durée maximale.
Quelle grille retenir ?
- Contenu figé au build et aucune donnée à enregistrer : pas de serveur, et un service tiers pour le formulaire.
- Un seul client, des données qui appartiennent à l’application et des traitements courts : tout dans Next.js, avec un CMS comme Payload installé dedans si besoin.
- Plusieurs clients, une API possédée par une autre équipe, des connexions longues ou des traitements lourds : une API séparée, que Next.js appelle depuis le serveur.
Dans le doute, commencer dans Next.js en isolant l’accès aux données dans un module laisse le choix ouvert. Le jour où un deuxième client arrive, ce module peut passer derrière une API sans réécrire les pages qui l’utilisent.