Skip to content
Learni
Voir tous les tutoriels
Développement Backend

Comment implémenter un Headless CMS avec Strapi en 2026

Read in English

Introduction

En 2026, les Headless CMS dominent les architectures modernes grâce à leur découplage total : un backend API-first (REST/GraphQL) servant n'importe quel frontend (web, mobile, IoT). Contrairement aux CMS monolithiques comme WordPress, Strapi – open-source et ultra-flexible – permet un contrôle granulaire via TypeScript, plugins et hooks personnalisés.

Ce tutoriel expert vous guide pas à pas pour déployer Strapi v5 avec Postgres, créer des content-types scalables (ex: articles avec relations), activer GraphQL et users-permissions, puis intégrer le tout dans une app Next.js 15 App Router avec queries optimisées, SSR et authentification JWT. Imaginez un blog multilingue où les webhooks synchronisent instantanément les drafts via Realtime.

Pourquoi c'est crucial ? Pour des perf à 99.99% uptime, SEO explosif via SSG/ISR et coûts AWS 70% inférieurs. Résultat : un CMS production-ready en 2h, bookmarké par tout lead dev. (128 mots)

Prérequis

  • Node.js 20+ et Yarn 4 (pnpm recommandé pour lockfiles)
  • PostgreSQL 16 local ou Docker (port 5432)
  • Next.js 15+ avec TypeScript et App Router
  • Compétences avancées : TypeScript, GraphQL, Prisma-like schemas, JWT/OAuth
  • Outils : Git, Docker Compose, Vercel/Netlify pour deploy

1. Initialiser le projet Strapi

terminal
npx create-strapi-app@latest cms-headless --quickstart --no-run
cd cms-headless
yarn add @strapi/provider-upload-cloudinary @strapi/plugin-graphql @strapi/plugin-users-permissions
yarn strapi install graphql users-permissions

Cette commande crée un projet Strapi v5 avec SQLite par défaut (switch vers Postgres ensuite). On ajoute les plugins GraphQL pour queries flexibles et users-permissions pour auth RBAC. Évitez --quickstart en prod ; utilisez --template pour custom seed. Lancez yarn develop pour admin UI sur localhost:1337.

2. Configurer PostgreSQL via Docker

Pour une DB scalable, remplacez SQLite par Postgres. Créez un docker-compose pour isolation. Cela évite les conflits locaux et simule prod (AWS RDS). Exposez le port 5432 et seed initial avec users admin.

docker-compose.yml pour DB

docker-compose.yml
version: '3'
services:
  postgres:
    image: postgres:16
    restart: always
    environment:
      POSTGRES_DB: strapi
      POSTGRES_USER: strapi
      POSTGRES_PASSWORD: strapi2026
    ports:
      - '5432:5432'
    volumes:
      - postgres_data:/var/lib/postgresql/data
volumes:
  postgres_data:

Ce compose lance Postgres persistant avec volumes. Utilisez psql pour vérifier : docker exec -it psql -U strapi -d strapi. Piège : oubliez les migrations Strapi post-config DB, sinon schemas perdus. docker-compose up -d && sleep 10.

config/database.ts

config/database.ts
export default ({ env }) => ({
  connection: {
    client: 'postgres',
    connection: {
      host: env('DATABASE_HOST', '127.0.0.1'),
      port: env.int('DATABASE_PORT', 5432),
      database: env('DATABASE_NAME', 'strapi'),
      user: env('DATABASE_USERNAME', 'strapi'),
      password: env('DATABASE_PASSWORD', 'strapi2026'),
      ssl: env.bool('DATABASE_SSL', false),
    },
    debug: false,
    acquireConnectionTimeout: 60000,
  },
});

Configure Strapi pour Postgres avec fallback env vars. Copiez dans .env : DATABASE_HOST=localhost etc. yarn build && yarn develop déclenche migrations auto. Attention : ssl=true en prod (RDS), sinon connexions refusées.

3. Créer un content-type Article scalable

Accédez à l'admin UI (http://localhost:1337/admin) > Content-Type Builder > + Article. Pour expert, définissez via fichiers TS : relations one-to-many (auteur), composants réutilisables (SEO), locales (i18n). Cela versionne Git et CI/CD.

schema.ts pour Article

src/api/article/content-types/article/schema.ts
export default {
  kind: 'collectionType',
  collectionName: 'articles',
  info: {
    singularName: 'article',
    pluralName: 'articles',
    displayName: 'Article',
    description: '',
  },
  options: {
    draftAndPublish: true,
  },
  pluginOptions: {
    i18n: {
      localizedFields: ['title', 'content'],
    },
  },
  attributes: {
    title: {
      type: 'string',
      required: true,
    },
    slug: {
      type: 'uid',
      targetField: 'title',
    },
    content: {
      type: 'richtext',
    },
    author: {
      type: 'relation',
      relation: 'manyToOne',
      target: 'api::user.user',
    },
    publishedAt: {
      type: 'datetime',
    },
  },
};

Définit un Article avec draft/publish, i18n, slug auto, rich-text et relation User (via plugin). yarn strapi dev recharge auto. Piège : sans draftAndPublish, pas de staging ; testez avec seeders pour populate.

config/plugins.ts pour auth & GraphQL

config/plugins.ts
export default {
  'users-permissions': {
    config: {
      jwt: {
        expiresIn: '7d',
      },
      register: {
        validatePasswordStrength: true,
      },
    },
  },
  graphql: {
    config: {
      endpoint: '/graphql',
      shadowCRUD: true,
      playgroundAlways: false,
      depthLimit: 10,
      amountLimit: 100,
      apolloServer: {
        tracing: false,
      },
    },
  },
};

Active JWT 7j, password strength et GraphQL avec depth/amount limits anti-DoS. /admin crée superadmin. Évitez playgroundAlways=true en prod (expose schemas). Test : curl POST /graphql avec introspection.

4. Intégrer Strapi dans Next.js App Router

Créez un frontend Next.js pour consommer l'API. Utilisez fetch natif + cache: 'no-store' pour realtime, ou SWR. Pour expert : client TS typé, middleware auth, ISR pour blogs statiques.

Installation Next.js + deps

terminal-frontend
npx create-next-app@15 frontend-headless --ts --app --tailwind --eslint --no-src-dir
cd frontend-headless
yarn add graphql-request @apollo/client @tanstack/react-query swr
yarn add -D @types/node

Init Next.js 15 App Router avec TS/Tailwind. Ajoute GraphQL clients : graphql-request lightweight, Apollo full-featured. react-query/SWR pour caching offline-first. yarn dev sur 3000.

lib/strapi-client.ts

lib/strapi-client.ts
import { GraphQLClient } from 'graphql-request';

const STRAPI_URL = process.env.NEXT_PUBLIC_STRAPI_URL || 'http://localhost:1337';

export const strapiClient = new GraphQLClient(`${STRAPI_URL}/graphql`);

export async function getArticles({ token }: { token?: string }) {
  strapiClient.setHeader('Authorization', token ? `Bearer ${token}` : '');
  const query = /* GraphQL */ `
    query Articles {
      articles(pagination: { limit: 10 }, publicationState: LIVE, sort: "publishedAt:desc") {
        data {
          id
          attributes {
            title
            slug
            content
            publishedAt
            author {
              data {
                attributes { name }
              }
            }
          }
        }
      }
    }
  `;
  const { articles } = await strapiClient.request(query);
  return articles;
}

export async function login(email: string, password: string) {
  const mutation = /* GraphQL */ `
    mutation Login($input: UsersPermissionsLoginInput!) {
      login(input: $input) {
        jwt
        user {
          id
          username
        }
      }
    }
  `;
  const data = await strapiClient.request(mutation, { input: { identifier: email, password } });
  return data.login;
}

Client GraphQL typé avec auth Bearer. Query paginée/live-only, sorted. Login mutation pour JWT. Env NEXT_PUBLIC_STRAPI_URL pour prod. Piège : oubliez publicationState=LIVE, drafts publics visibles.

app/blog/page.tsx

app/blog/page.tsx
import { getArticles } from '@/lib/strapi-client';

export default async function BlogPage() {
  const { articles } = await getArticles({});

  return (
    <div className="container mx-auto p-8">
      <h1 className="text-4xl font-bold mb-8">Articles</h1>
      <div className="grid md:grid-cols-2 gap-6">
        {articles.data.map((article: any) => (
          <article key={article.id} className="border p-6 rounded-lg">
            <h2 className="text-2xl font-semibold mb-2">{article.attributes.title}</h2>
            <p className="text-gray-600 mb-4">Par {article.attributes.author?.data?.attributes?.name}</p>
            <div className="prose max-w-none" dangerouslySetInnerHTML={{ __html: article.attributes.content.substring(0, 200) + '...' }} />
            <time>{new Date(article.attributes.publishedAt).toLocaleDateString()}</time>
          </article>
        ))}
      </div>
    </div>
  );
}

export const revalidate = 3600; // ISR 1h

Page SSR/ISR fetchant articles live. Tailwind styling, rich-text preview safe. revalidate pour hybrid static/dynamic. Pour auth : passez token via cookies/middleware. Test : publiez Article en admin.

Bonnes pratiques

  • Rate limiting & CORS : Ajoutez nginx ou Strapi middleware (config/middlewares.ts) pour 100 req/min/IP.
  • Migrations Git : Versionnez schemas.ts, utilisez strapi export pour DB snapshots.
  • Realtime : Webhooks sur entry.update → Pusher/Supabase pour live previews.
  • SEO/Perf : ISR + Next Image pour articles, CDNs Cloudinary pour media.
  • Sécurité : RBAC granulaire (roles custom), OWASP ZAP scans, secrets Vault.

Erreurs courantes à éviter

  • DB non-migrée : yarn build sans DB config = crash 500 ; toujours strapi db:migrate.
  • GraphQL depth explosion : Sans limits, N+1 queries DoS ; fixez depthLimit=7.
  • JWT leaks : Stocker en httpOnly cookies, pas localStorage (XSS vuln).
  • CORS wildcards : '*' expose à CSRF ; whitelist domaines frontend prod/staging.

Pour aller plus loin

Approfondissez avec Strapi v5 migrations, GraphQL Federation multi-CMs. Déployez sur Railway/Render (1-click Postgres).

Découvrez nos formations Learni expertes Next.js & Headless pour masterclass live coding et audits perf.