Apogée Consult
Retour au blog
Mathieu Ponton
Mathieu PontonCo-Founder & ingénieur logiciel

Je suis Mathieu Ponton, Co-Founder & ingénieur logiciel chez Apogée Consult à Lyon. Ingénieur diplômé de Polytech Lyon (Informatique), j'ai fait trois ans en apprentissage, partagés entre la Métropole de Lyon (inclusion numérique avec Res'in et sobriété énergétique avec Écolyo) et Superwyze, une startup medtech (POCs, dont certains aujourd'hui industrialisés, et travail sur des codebases existantes). J'ai livré plus de 10 projets en production (web, mobile et IA / RAG) pour des PME, startups et organisations publiques.

Payload 3 + Next.js après 8 mois : ce qu'on referait et ce qu'on changerait

  • payload

Huit mois à livrer des projets avec Payload 3 et Next.js App Router. Ce qui a tenu, ce qui a surpris, ce qu'on ne referait pas à l'identique.

Payload 3 + Next.js après 8 mois : ce qu'on referait et ce qu'on changerait

Huit mois, trois projets livrés en prod, deux encore en développement actif. Payload 3 avec l'App Router de Next.js, c'est le socle sur lequel on a parié début 2025. Voici ce que ce pari vaut vraiment, sans la partie commerciale.

Ce qu'on referait sans hésiter

La colocalistion du CMS dans le projet Next.js

La grande promesse de Payload 3 : le CMS vit dans le même dépôt que le front. Un seul next build, une seule image Docker, un seul déploiement.

En pratique, ça change radicalement la façon dont on itère. Ajouter un champ à une collection, c'est une modification TypeScript dans le même dépôt que le composant qui l'affiche. Pas de va-et-vient entre deux repos, pas de désynchronisation de types.

Le typage end-to-end via @payloadcms/next génère automatiquement les types depuis les collections :

// types auto-générés dans payload-types.ts
export interface Page {
  id: string;
  title: string;
  hero: {
    heading: string;
    image: Media;
  };
  updatedAt: string;
  createdAt: string;
}

Sur un projet de 40 collections, on a récolté une douzaine de régressions stoppées à la compilation qui auraient été des bugs en prod avec une approche API REST non typée.

L'accès direct aux données depuis les Server Components

On peut appeler payload.find() directement dans un Server Component, sans passer par une API HTTP intermédiaire :

// app/(frontend)/[slug]/page.tsx
import { getPayload } from 'payload';
import configPromise from '@payload-config';

export default async function Page({ params }: { params: { slug: string } }) {
  const payload = await getPayload({ config: configPromise });

  const page = await payload.find({
    collection: 'pages',
    where: { slug: { equals: params.slug } },
    limit: 1,
  });

  if (!page.docs.length) notFound();
  return <PageTemplate doc={page.docs[0]} />;
}

Pas de latence réseau supplémentaire, pas de sérialisation JSON inutile, pas de gestion de token d'API côté front. Sur les pages marketing avec beaucoup de relations imbriquées, on gagne entre 40 et 120 ms par requête comparé à une architecture découplée.

Les migrations auto-générées

payload migrate:create inspecte l'état courant des collections et génère le fichier de migration SQL. Ça couvre 90 % des cas sans qu'on ait à écrire une ligne de SQL à la main. Combiné à Drizzle sous le capot, le résultat est lisible et auditable.

Ce qu'on changerait

L'organisation des fichiers de collection

On a commencé avec une collection par fichier, tous dans /src/collections/. Mauvaise idée sur un projet qui grossit. À 25 collections, le dossier devient ingérable.

Ce qu'on fait maintenant : regrouper par domaine.

src/
  collections/
    content/
      Pages.ts
      Posts.ts
      Categories.ts
    media/
      Media.ts
      Documents.ts
    crm/
      Users.ts
      Organizations.ts
      Contacts.ts

C'est un changement de structure pure, aucun impact sur Payload, mais il faut y penser dès le début.

Les hooks globaux sur les globals de Payload

Sur notre premier projet, on a accroché des hooks afterChange sur les globals pour invalider le cache Next.js. Ça fonctionnait, mais c'était fragile : un revalidatePath lancé depuis un hook Payload dans un contexte qui n'est pas celui du serveur Next.js peut silencieusement échouer sans lever d'exception.

La solution qu'on a adoptée : passer par une route API dédiée /api/revalidate appelée en interne, avec une clé secrète, plutôt que d'appeler revalidatePath directement depuis un hook.

// src/collections/Pages/hooks/revalidatePage.ts
export const revalidatePage: AfterChangeHook = async ({ doc }) => {
  try {
    await fetch(`${process.env.NEXT_PUBLIC_SITE_URL}/api/revalidate`, {
      method: 'POST',
      headers: { 'x-revalidate-secret': process.env.REVALIDATE_SECRET! },
      body: JSON.stringify({ slug: doc.slug }),
    });
  } catch (err) {
    console.error('[revalidate] Échec silencieux évité :', err);
  }
};

La gestion des médias en production

Payload gère l'upload local par défaut. En prod sur un déploiement multi-instance ou serverless, ça ne tient pas. On l'a découvert trop tard sur un projet, après avoir livré avec le plugin @payloadcms/plugin-cloud-storage mal configuré.

Aujourd'hui, on configure S3 dès le premier payload.config.ts, avant même d'écrire la première collection. Le plugin existe, il fonctionne bien, mais le configurer après coup demande de migrer des assets déjà uploadés.

Les performances de l'admin sur les grandes collections

L'interface admin de Payload 3 est raisonnable pour des collections de quelques centaines de documents. Au-delà de 5 000 documents avec des relations imbriquées, la liste devient lente. On a dû ajouter des index manuellement sur les colonnes de tri et de filtre, ce que les migrations auto-générées ne font pas d'elles-mêmes.

// dans la collection
indexes: [
  { fields: ['createdAt'] },
  { fields: ['status', 'publishedAt'] },
],

Ce n'est pas un défaut rédhibitoire, mais c'est un angle mort que la documentation n'aborde pas.

Les zones d'ombre qu'on surveille encore

Le Local API n'est pas disponible côté client. C'est logique, elle tourne côté serveur, mais ça signifie que toutes les interactions client (formulaires, mutations) doivent passer par les routes API de Payload ou des routes API Next.js custom. Sur des projets avec beaucoup d'interactivité, on se retrouve à reconstruire une couche intermédiaire.

Le cycle de vie des plugins n'est pas documenté en détail. On a eu des comportements inattendus sur l'ordre d'exécution des hooks quand deux plugins modifient la même collection. On a dû lire le code source pour comprendre.

Payload 3 est encore jeune. Le changelog entre les versions mineures contient régulièrement des breaking changes signalés comme "fixes". On épingle toujours la version exacte ("payload": "3.x.y") et on ne met à jour qu'après lecture du diff.

Ce qu'on recommande à un CTO qui évalue aujourd'hui

Si votre projet est un site marketing, une application SaaS avec un besoin éditorial modéré ou un back-office custom sur stack TypeScript, Payload 3 est un très bon choix. L'écosystème Next.js s'intègre sans friction, le typage end-to-end vaut l'investissement initial.

Si vous avez des contraintes de scaling extrêmes (millions de documents), des équipes éditoriales non-techniques qui ont besoin d'une expérience admin très polie, ou un besoin de déploiement multi-région, évaluez d'abord si l'admin Payload répond à vos besoins sans customisation lourde.

La question qu'on se pose encore : comment Payload gèrera le passage à grande échelle quand l'écosystème de plugins aura mûri ? On n'a pas la réponse.

Disponible pour de nouveaux projets

Un projet à concrétiser ?
Parlons-en, sans engagement.

Un échange de 30 minutes pour cadrer votre besoin, qualifier la faisabilité et vous proposer une trajectoire claire.

1// kick-off : réponse sous 24h
2const project = await apogee.scope({
3 type: 'web | mobile | IA',
4 timeline: '6 à 16 semaines',
5 approach: 'sur-mesure'
6})
7// → cadrage offert
Payload 3 + Next.js : retour d'expérience après 8 mois | Apogée Consult