Lecture : 13 minutes · Niveau : intermédiaire · Mise à jour : avril 2026
Le cœur de Strapi, ce sont les content types et l’API qu’ils exposent. Les bases s’apprennent en quelques heures, mais bien modéliser des structures complexes et exploiter l’API au mieux demande de la pratique. Ce guide couvre les patterns vraiment utiles : composants, dynamic zones, populate, filtres avancés, lifecycle hooks pour automatiser des comportements.
Voir aussi → Strapi headless CMS pour PME : guide complet.
Sommaire
- Modélisation : philosophie et conventions
- Champs simples et leurs subtilités
- Relations : one-to-many, many-to-many, polymorphic
- Components réutilisables
- Dynamic Zones pour pages modulaires
- API REST : populate, filtres, sort, pagination
- API GraphQL avancée
- Lifecycle hooks et logique métier
- Custom controllers et services
- Validation et sanitization
- FAQ
1. Modélisation : philosophie et conventions
Bien modéliser ses content types évite des refactorings douloureux plus tard.
Principes
- Penser API consumer-first : comment le frontend va-t-il consommer ce contenu ?
- Pas de duplication : si un champ est partagé entre plusieurs types, en faire un component ou une relation
- Unité de la donnée : un content type = un concept métier
- Évolutivité : prévoir l’extension future plutôt que tout enfermer dès le début
Conventions de nommage
- Singular API ID : nom du content type au singulier (
article, pasarticles) - Plural API ID : Strapi gère automatiquement (
articles) - Champs en snake_case ou camelCase : choisir une convention et s’y tenir
- Components dans des catégories : grouper logiquement (
shared.seo,blocks.hero,forms.contact)
Single Type vs Collection Type
- Collection Type : entité multiple (Article, Produit, Client, Page)
- Single Type : entité unique (Configuration globale, Page d’accueil, Footer)
Single Types parfaits pour des données globales. Évite la confusion d’avoir une seule entrée dans une collection.
2. Champs simples et leurs subtilités
Text vs Rich Text
- Text (short) : titre, slug, label
- Text (long) : excerpt, description courte
- Rich Text : contenu d’article. Format Markdown ou Blocks (éditeur structuré v5)
Le format Blocks (Strapi v5) est plus puissant : structure JSON éditée via UI dédiée, plus précis qu’un Markdown libre, mais plus complexe à rendre côté frontend.
Number
- Integer, BigInteger : entiers
- Decimal : monétaire (préférer à Float pour la précision)
- Float : flottants
Pour des prix : Decimal avec précision contrôlée.
Date
- Date : seulement la date (anniversaire, deadline)
- DateTime : avec heure (publication, événement précis)
- Time : heure seule
Toujours utiliser DateTime pour des timestamps réels.
Boolean
Utile pour des flags simples (publié/brouillon, actif/inactif). Pour des états multiples : utiliser un champ Enumeration plutôt qu’un boolean qui devient ambigu.
Enumeration
Liste fermée de valeurs. Très utile pour des status, types, catégories fixes. Plus contraint que Text avec validation, plus simple qu’une relation.
JSON
Pour des données structurées libres (paramètres, métadonnées variables). À utiliser avec parcimonie : moins requêtable qu’un schéma typé.
Media
- Single media : une seule image/fichier
- Multiple media : galerie d’images, fichiers attachés
Configurer les types de fichiers acceptés par champ (images uniquement, ou tous fichiers).
UID
Identifiant unique généré (souvent depuis le titre). Pour les slugs URL. Auto-update activable.
3. Relations : one-to-many, many-to-many, polymorphic
One-to-One
Article a un Auteur principal :
Article ←→ Author (one-to-one)
Rarement utilisé en pratique : préférer one-to-many quand possible.
One-to-Many
Catégorie contient plusieurs Articles :
Category 1 → N Articles
Article N → 1 Category
Configuration : sur Article, ajouter un champ Relation « Many-to-One » vers Category.
Many-to-Many
Article a plusieurs Tags, Tag est sur plusieurs Articles :
Article M → N Tags
Strapi gère automatiquement la table de jointure.
Polymorphic
Une Image peut être attachée à plusieurs types (Article, Page, Produit). Strapi gère via Media field. Pour des relations polymorphiques custom (commentaires sur articles ou pages, par exemple) : workaround via JSON ou tables séparées.
Performance des relations
Strapi N+1 par défaut sur les relations. Toujours utiliser populate (REST) ou sélection explicite (GraphQL) pour charger en une seule requête. Voir section API.
4. Components réutilisables
Les components sont des groupes de champs réutilisables.
Cas d’usage typiques
- SEO : meta_title, meta_description, og_image, canonical_url. À mettre sur tous les content types ayant une URL publique.
- Address : street, city, postal_code, country
- ContactInfo : email, phone, fax
- OpeningHours : repeatable component (un par jour de la semaine)
- CallToAction : title, link, button_text, button_style
Création
Content-Type Builder → Components → Create new component. Choisir une catégorie (group logique) et le nom.
Utilisation
Sur un content type, ajouter un champ « Component » et sélectionner le component créé. Option « Repeatable » pour permettre plusieurs instances (ex: liste d’horaires d’ouverture).
Avantages
- Cohérence : même structure partout où le component est utilisé
- Maintenabilité : modifier le component met à jour partout
- Réutilisation : une seule définition pour de nombreux types
Limites
- Pas de relations vers d’autres content types depuis un component (limitation à connaître)
- Pas indépendamment requêtable : les components sont toujours imbriqués dans un parent
5. Dynamic Zones pour pages modulaires
Les Dynamic Zones permettent à un éditeur de construire une page modulaire en empilant des sections de type variable.
Cas d’usage
Une page marketing flexible où l’éditeur peut combiner :
– Hero
– Texte avec image
– Galerie
– Témoignages
– Call-to-action
– FAQ
Configuration
Sur un content type Page : ajouter un champ « Dynamic Zone ». Sélectionner les components autorisés. L’éditeur peut alors empiler ces sections dans n’importe quel ordre.
Rendu côté frontend
// Next.js exemple
function PageRenderer({ blocks }) {
return blocks.map((block, i) => {
switch (block.__component) {
case 'blocks.hero':
return <Hero key={i} {...block} />;
case 'blocks.gallery':
return <Gallery key={i} {...block} />;
case 'blocks.cta':
return <CTA key={i} {...block} />;
default:
return null;
}
});
}
Le frontend distingue les types via le champ __component et rend le composant React approprié.
Avantages
- Flexibilité énorme pour les éditeurs
- Pas besoin de coder une nouvelle page pour chaque variation
- Réutilisation des sections sur plusieurs pages
Coûts
- Nécessite que le frontend implémente chaque type de bloc
- Plus complexe à débugger que des pages statiques
- Demande discipline dans la modélisation pour éviter le chaos
Pour des sites marketing dynamiques : excellent investissement.
6. API REST : populate, filtres, sort, pagination
Populate
Sans populate, les relations ne sont pas chargées. Toujours préciser explicitement.
GET /api/articles?populate=* # tout (déconseillé en prod)
GET /api/articles?populate=author,categories # ciblé
GET /api/articles?populate[author][populate]=avatar # imbriqué
GET /api/articles?populate[blocks][on][blocks.hero][populate]=image # dynamic zone
populate=* charge tout en cascade : à éviter en production (performance, taille payload). Toujours préciser ce dont le frontend a vraiment besoin.
Filtres
?filters[publishedAt][$notNull]=true
?filters[author][slug][$eq]=jean-doe
?filters[categories][name][$contains]=tech
?filters[$or][0][title][$contains]=guide&filters[$or][1][title][$contains]=tutoriel
Opérateurs : $eq, $ne, $lt, $gt, $lte, $gte, $contains, $in, $notIn, $null, $notNull, $between, $startsWith, $endsWith.
Sort
?sort=publishedAt:desc
?sort[0]=featured:desc&sort[1]=publishedAt:desc # multi-critères
Pagination
?pagination[page]=1&pagination[pageSize]=20
?pagination[start]=0&pagination[limit]=20
?pagination[withCount]=true
Sélection de champs
?fields=title,slug,excerpt
Pour ne charger que les champs nécessaires. Optimise le payload.
Locale
?locale=fr
?locale=all
Pour le multi-langue.
7. API GraphQL avancée
Avec le plugin GraphQL, plus flexible que REST.
query GetArticles($category: String!) {
articles(
filters: { categories: { slug: { eq: $category } } }
sort: "publishedAt:desc"
pagination: { page: 1, pageSize: 20 }
) {
data {
id
attributes {
title
slug
excerpt
publishedAt
coverImage {
data {
attributes {
url
alternativeText
formats
}
}
}
author {
data {
attributes { name avatar { data { attributes { url } } } }
}
}
categories {
data { attributes { name slug } }
}
}
}
meta { pagination { page pageCount total } }
}
}
Mutations
mutation CreateArticle($data: ArticleInput!) {
createArticle(data: $data) {
data { id attributes { title slug } }
}
}
Permissions et authentification s’appliquent comme sur REST.
Avantages GraphQL
- Une seule requête pour un graphe complet (vs plusieurs REST)
- Schema introspectable (auto-complétion dans IDE)
- Type safety si client typé
- Pas de over-fetching ni under-fetching
Inconvénients
- Plus complexe à mettre en cache (le caching CDN classique fonctionne moins bien)
- Courbe d’apprentissage
- Outillage côté frontend (Apollo, urql) à intégrer
Pour des apps avec relations complexes : GraphQL gagne. Pour des cas simples : REST suffit.
8. Lifecycle hooks et logique métier
Strapi expose des hooks au cycle de vie des entités.
Hooks disponibles
beforeCreate,afterCreatebeforeUpdate,afterUpdatebeforeDelete,afterDeletebeforeFindOne,afterFindOnebeforeFindMany,afterFindManybeforeCount,afterCount
Implémentation
src/api/article/content-types/article/lifecycles.js :
module.exports = {
async beforeCreate(event) {
const { data } = event.params;
if (!data.slug && data.title) {
data.slug = slugify(data.title);
}
},
async afterCreate(event) {
const { result } = event;
await strapi.entityService.create('api::audit-log.audit-log', {
data: { action: 'article_created', article: result.id, user: ctx.state.user.id },
});
},
async beforeUpdate(event) {
// ex: incrémenter une version, mettre à jour un champ updated_by
},
};
Cas d’usage typiques
- Slugification automatique
- Audit log
- Notifications (email, webhook) après publication
- Calculs dérivés (ex: word_count, reading_time)
- Sanitization de champs
- Validation métier au-delà des validations natives
Attention performance
Les hooks s’exécutent à chaque opération. Lourd ou bloquant = ralentissement API. Pour des actions coûteuses : déléguer à un job asynchrone (Bull, BullMQ, ou autre queue).
9. Custom controllers et services
Pour aller au-delà des routes par défaut.
Controller custom
src/api/article/controllers/article.js :
const { createCoreController } = require('@strapi/strapi').factories;
module.exports = createCoreController('api::article.article', ({ strapi }) => ({
async findFeatured(ctx) {
const articles = await strapi.entityService.findMany('api::article.article', {
filters: { featured: true, publishedAt: { $notNull: true } },
sort: 'publishedAt:desc',
limit: 5,
populate: ['author', 'coverImage'],
});
return { data: articles };
},
}));
Route custom
src/api/article/routes/custom.js :
module.exports = {
routes: [
{
method: 'GET',
path: '/articles/featured',
handler: 'article.findFeatured',
config: { policies: [], auth: false },
},
],
};
Ainsi GET /api/articles/featured retourne uniquement les articles vedettes, optimisé.
Services
Les services Strapi factorisent la logique business réutilisable entre controllers, hooks, plugins. Pattern standard MVC adapté à Strapi.
module.exports = createCoreService('api::article.article', ({ strapi }) => ({
async getRelatedArticles(article) {
// logique de recommandation
},
}));
Appel depuis controller : strapi.service('api::article.article').getRelatedArticles(...).
10. Validation et sanitization
Validations natives
Sur chaque champ, configurable dans Content-Type Builder :
- required
- unique
- min/max length pour text
- min/max value pour number
- regex pattern
- email format
- default value
Validations custom
Via lifecycle hooks beforeCreate / beforeUpdate :
async beforeCreate(event) {
const { data } = event.params;
if (data.startDate && data.endDate && data.startDate > data.endDate) {
throw new ValidationError('startDate must be before endDate');
}
}
Sanitization
Strapi sanitize automatiquement les inputs (XSS, injection). Pour des cas spécifiques (whitelist de tags HTML autorisés dans rich text), configurer le plugin upload ou le rich text editor.
Permissions par champ
Dans Settings → Roles, on peut désactiver des champs spécifiques pour certains rôles. Un éditeur peut modifier le titre mais pas le statut publication, par exemple.
11. FAQ
populate=* en prod ou pas ?
Non. Charge toutes les relations en cascade, payload énorme, performance dégradée. Toujours préciser explicitement les champs nécessaires.
Components ou content types séparés ?
Components quand le contenu est intégré au parent (SEO, address). Content type séparé quand on veut le requêter, le réutiliser via relations, ou avoir une URL publique dédiée.
Dynamic Zones partout ?
Non, ça devient vite chaotique. Réserver aux pages vraiment modulaires (landing, marketing). Pour des structures stables (article de blog), un schéma fixe est plus clair.
Lifecycle hooks ralentissent l’API ?
Oui si lourds. Garder les hooks rapides (calculs simples, validations). Pour actions lourdes (envoi email, appel API tiers) : queue asynchrone.
Migration de schema sans perdre les données ?
Strapi génère des migrations automatiques pour la plupart des changements. Renommage de champ, changement de type, suppression : à tester sur staging avant production.
REST vs GraphQL : impact sur le cache ?
REST : facile à cacher au niveau CDN/HTTP par URL. GraphQL : tout sur même endpoint POST, cache HTTP standard ne marche pas. Solutions : Apollo cache côté client, ou GraphQL caching server-side.
Comment versionner les content types ?
Changements de schéma génèrent des migrations dans database/migrations. Versionner ces migrations dans Git, jamais les modifier après application.
Articles liés (cluster Strapi)
- 👉 Strapi headless CMS pour PME : guide complet (pillar)
- 👉 Strapi déploiement production
- 👉 Strapi + Next.js intégration
Article mis à jour le 25 avril 2026. Pour signaler une erreur ou suggérer une amélioration, écrivez-nous.