Next.js & full-stack · Publié le · 9 min

Architecture Next.js pour un SaaS en production : les décisions que je prends sur chaque produit

Ce qui sépare un projet Next.js qui tient de celui qu’on réécrit au bout d’un an tient à une dizaine de décisions. Voici les miennes, telles qu’elles sont appliquées sur Atamatax, Horvera, Wabiro, SponsorsMetrics — et sur ce site.

  • Next.js
  • Architecture
  • SaaS

Next.js permet de livrer un produit web complet avec une seule personne. Il permet aussi de livrer une démo qui a l’air d’un produit. La différence n’est pas dans le cadre ; elle est dans une dizaine de décisions prises — ou pas — au début. Voici celles que j’applique sur chaque produit que j’exploite, et sur ce site, dont vous pouvez inspecter le code source rendu.

Le modèle de données avant les écrans

La première semaine d’un produit ne produit aucun écran. Elle produit un schéma Postgres : les entités, à qui elles appartiennent, ce qui est immuable, ce qui est versionné, les contraintes et les index. Les migrations sont versionnées dans le repo et s’exécutent dans le pipeline. C’est la seule partie du produit que le reste ne peut pas contourner, donc la seule qu’il est dangereux de laisser floue.

Sur Horvera, les aéroports, les compagnies et leur champ d’application sont des données de référence ; le moteur de règles et les pages programmatiques les lisent depuis la même source, ce qui garantit qu’une page ne promet jamais ce que le moteur refuserait. Cette cohérence est une décision de schéma, pas de code.

Composants serveur par défaut, client par exception

Avec l’App Router, un composant est rendu sur le serveur sauf s’il déclare le contraire. Je garde cette règle stricte : une page lit ses données là où elle les affiche, envoie du HTML lisible par les moteurs sans exécution, et n’embarque du JavaScript que pour l’interactivité réelle. Ce site en est l’exemple extrême — chaque composant est un composant serveur, et la couche interactive est un script natif différé de quelques kilo-octets.

Le bénéfice n’est pas seulement la performance. Un composant serveur ne peut pas fuiter un secret vers le navigateur, ne peut pas être appelé avec des données que le serveur n’a pas validées, et se teste comme une fonction. Le composant client reste nécessaire — un calculateur, un test interactif comme sur Wabiro — mais il est l’exception qu’on justifie.

Les formulaires sont des server actions — et fonctionnent sans JavaScript

Un formulaire est un `<form>` lié à une action serveur. Il fonctionne sans JavaScript : la soumission est un POST, l’action valide les champs avec Zod, applique une limite de débit par IP, vérifie un champ pot-de-miel, envoie ce qu’elle doit envoyer, puis redirige vers l’URL de résultat que la page rend comme un état. C’est exactement ainsi que fonctionne le formulaire de contact de ce site, et les formulaires de la Build School.

Il n’y a pas de couche API à maintenir pour chaque écran, pas d’état de chargement à synchroniser, pas de double soumission possible sans qu’elle soit gérée côté serveur. Quand du JavaScript est présent, Next.js améliore progressivement le formulaire ; quand il ne l’est pas, rien ne casse.

Des route handlers pour ce qu’un tiers appelle

Les webhooks Stripe, les intégrations et les endpoints publics sont des route handlers — pas des actions. Ils vérifient une signature, sont idempotents (un webhook reçu deux fois ne facture pas deux fois) et journalisent ce qu’ils ont fait. Sur ce site, le webhook Stripe de la Build School vérifie la signature HMAC avec le module crypto de Node, sans SDK : trente lignes qu’on peut lire en entier.

L’état de facturation vit dans la base de données, écrit par le webhook, jamais déduit côté client. Une page qui affiche « abonnement actif » lit la base ; elle ne demande pas à Stripe.

La logique métier dans des modules purs, versionnés

Une règle d’éligibilité, un calcul de commission, un scoring : tout cela vit dans des fonctions pures, sans dépendance à React ni à la requête, testées contre des scénarios réels et versionnées. Le moteur de règles d’Horvera produit chaque réponse avec sa trace et ses sources ; le scoring de Wabiro est un module qui s’exécute dans le navigateur et qui produit aussi les pages de méthode ; le moteur de situation fiscale d’Atamatax affiche ses hypothèses et sa confiance.

La conséquence pratique : une règle qui change se modifie à un endroit, avec une version, et les résultats passés restent explicables. Une fonctionnalité IA, quand elle est justifiée, s’insère dans ce cadre comme une fonction typée de plus — j’en parle dans Quand un SaaS doit-il utiliser l’IA.

Authentification et autorisation côté serveur

Les sessions sont vérifiées sur le serveur, à chaque lecture de données — pas seulement à l’entrée d’une page. Les permissions sont modélisées en base (compte, rôle, ressource) et appliquées dans les requêtes. Une interface qui cache un bouton n’est pas une autorisation. Le proxy Next.js sert à router et à protéger des groupes de routes ; il ne remplace pas la vérification dans chaque accès aux données.

Cache et rendu, page par page

Chaque page a une stratégie de rendu choisie : statique quand le contenu le permet, dynamique quand il dépend de l’utilisateur, revalidée quand une donnée change. Le piège est d’hériter d’un comportement par défaut sans le savoir — une page marketing rendue à chaque requête, ou un tableau de bord mis en cache entre deux utilisateurs. Je note la stratégie de chaque route dans le code, et je la vérifie dans les en-têtes de réponse en production.

Le SEO comme sortie de l’architecture

Les métadonnées, le sitemap, les canonicals, le hreflang et le JSON-LD sont dérivés des mêmes données que les pages. Sur ce site, le sitemap lit la liste des articles, des projets et des services ; le routage des langues se fait dans le proxy, qui réécrit `/en/...` vers la route française avec la locale en paramètre ; les données structurées sont générées par des fonctions qui lisent le contenu affiché. Rien n’est écrit deux fois, donc rien ne peut se contredire.

Sur Horvera, les pages programmatiques — compagnie, aéroport, route, type de perturbation, juridiction — sont générées depuis les données de référence du moteur. Le SEO n’est pas une couche ajoutée ; c’est ce que produit une architecture où les données sont au bon endroit.

L’observabilité dès le premier déploiement

Journaux structurés, alertes d’erreur, événements produit (PostHog ou Vercel Analytics), journal d’audit sur les actions sensibles — branchés au premier déploiement, pas au premier incident. Le déploiement est sur Vercel avec une preview par branche : les démos du vendredi se font sur ces previews, avec les vraies variables d’environnement de préproduction.

Ce que je ne fais pas

  • Pas de microservices, pas de file de messages, pas de cache distribué avant qu’un vrai goulot ne les justifie. Un Postgres bien indexé porte loin.
  • Pas d’abstraction maison au-dessus de Next.js. La structure standard de l’App Router est ce qu’un autre développeur saura lire.
  • Pas de SDK quand un appel REST signé fait la même chose en trente lignes lisibles.
  • Pas de rendu client pour des pages que les moteurs doivent lire.

En une ligne

Données d’abord, serveur par défaut, formulaires sans JavaScript, webhooks signés et idempotents, logique métier pure et versionnée, autorisation dans les requêtes, cache choisi, SEO dérivé, observabilité dès le début. C’est ce que je construis quand on me confie une application Next.js — et c’est ce que vous pouvez vérifier sur les produits en bas de cette page.

Construit avec cette approche

FIELD NOTES · À APPLIQUER

Le service correspondant

Besoin de l’implémenter dans un produit de production ?

Vérifier l’adéquation du projet

Six questions, une orientation immédiate, puis une revue de périmètre gratuite.