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
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-permissionsCette 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
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 . Piège : oubliez les migrations Strapi post-config DB, sinon schemas perdus. docker-compose up -d && sleep 10.
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
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
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
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/nodeInit 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
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
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 1hPage 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.