
Introduction
Strapi est l'une des plateformes CMS headless open-source les plus populaires, mais c'est aussi une énorme base de code avec des centaines de contributeurs et des milliers de "pull requests". Il n'est pas facile de maintenir la qualité d'un projet d'une telle ampleur. Il faut des règles de révision du code claires et cohérentes pour s'assurer que chaque contribution reste fiable, lisible et sécurisée.
Dans cet article, nous avons rassemblé un ensemble de règles de révision de code basées sur le dépôt public de Strapi. Ces règles proviennent d'un travail réel : des problèmes, des discussions et des demandes d'extraction qui ont aidé le projet à grandir tout en gardant la base de code stable.
Pourquoi il est difficile de maintenir la qualité du code dans un grand projet open-source
Le maintien de la qualité dans un grand projet open source est un défi en raison de l'ampleur et de la diversité des contributions. Des centaines, voire des milliers de développeurs, qu'ils soient bénévoles ou ingénieurs chevronnés, soumettent des demandes de modification, chacune introduisant de nouvelles fonctionnalités, des corrections de bogues ou des remaniements. En l'absence de règles claires, la base de code peut rapidement devenir incohérente, fragile ou difficile à parcourir.
Voici quelques-uns des principaux défis à relever :
- Des contributeurs diversifiés avec des niveaux d'expérience variés.
- Modèles de codage incohérents d'un module à l'autre.
- Les bogues cachés et la duplication de la logique s'insinuent.
- Risques pour la sécurité si les processus ne sont pas appliqués.
- Des révisions qui prennent beaucoup de temps pour les volontaires qui ne connaissent pas l'ensemble du code.
Pour relever ces défis, les projets réussis s'appuient sur des processus structurés : des normes partagées, des outils automatisés et des lignes directrices claires. Ces pratiques garantissent la maintenabilité, la lisibilité et la sécurité, même lorsque le projet grandit et attire davantage de contributeurs.
Comment le respect de ces règles améliore la maintenabilité, la sécurité et l'accueil des nouveaux arrivants ?
Le respect d'un ensemble clair de règles d'examen du code a un impact direct sur la santé de votre projet :
- Facilité de maintenance : La cohérence des structures de dossiers, des conventions de dénomination et des modèles de codage facilite la lecture, la navigation et l'extension de la base de code.
- Sécurité : La validation des entrées, l'assainissement, les contrôles d'autorisation et l'accès contrôlé aux bases de données réduisent les vulnérabilités et empêchent les fuites accidentelles de données.
- Une intégration plus rapide : Des normes partagées, des utilitaires documentés et des exemples clairs aident les nouveaux collaborateurs à comprendre rapidement le projet et à y contribuer en toute confiance.
En appliquant ces règles, les équipes peuvent s'assurer que la base de code reste évolutive, fiable et sécurisée, même si le nombre de contributeurs augmente.
Faire le lien entre le contexte et les règles
Avant d'examiner les règles, il est important de comprendre que le maintien d'une qualité de code élevée dans un projet comme Strapi ne consiste pas seulement à suivre les meilleures pratiques générales. Il s'agit d'avoir des modèles et des normes clairs qui aident des centaines de contributeurs à rester sur la même longueur d'onde. Chacune des 20 règles ci-dessous se concentre sur des défis réels qui apparaissent dans la base de code de Strapi.
Les exemples fournis pour chaque règle illustrent à la fois des approches non conformes et des approches conformes, ce qui donne une image claire de la manière dont ces principes s'appliquent dans la pratique.
Explorons maintenant les règles qui rendent la base de code de Strapi évolutive, cohérente et de haute qualité, en commençant par la structure du projet et les normes de configuration.
Règles : Structure et cohérence du projet
1. Suivre les conventions de Strapi en matière de dossiers
Évitez de disperser les fichiers ou d'inventer de nouvelles structures. Respectez la structure du projet établie par Strapi pour que la navigation reste prévisible.
❌ Exemple de non-conformité
1src/
2├──controllers/
3│└── userController.js
4├──services/
5│└── userLogic.js
6├──routes/
7│└── userRoutes.js
8└──utils/
9 └─── helper.js✅ Exemple de conformité
1src/
2└──api/
3 └── user/
4 ├─── controllers/
5 │ └─── user.js
6 ├── services/
7 │ └─── user.js
8 ├── routes/
9 │ └─── user.js
10 └── content-types/
11 └── user/schema.json2. Maintenir la cohérence des fichiers de configuration
Utilisez la même structure, les mêmes noms et les mêmes conventions de formatage dans tous les fichiers de configuration afin de garantir la cohérence et d'éviter les erreurs.
❌ Exemple de non-conformité
1// config/server.js
2module.exports = {
3 PORT: 1337,
4 host: '0.0.0.0',
5 APP_NAME: 'my-app'
6}
7
8// config/database.js
9export default {
10 connection: {
11 client: 'sqlite',
12 connection: { filename: '.tmp/data.db' }
13 }
14}
15
16// config/plugins.js
17module.exports = ({ env }) => ({
18 upload: { provider: "local" },
19 email: { provider: 'sendgrid' }
20});✅ Exemple de conformité
1// config/server.js
2module.exports = ({ env }) => ({
3 host: env('HOST', '0.0.0.0'),
4 port: env.int('PORT', 1337),
5 app: { keys: env.array('APP_KEYS') },
6});
7
8// config/database.js
9module.exports = ({ env }) => ({
10 connection: {
11 client: 'sqlite',
12 connection: { filename: env('DATABASE_FILENAME', '.tmp/data.db') },
13 useNullAsDefault: true,
14 },
15});
16
17// config/plugins.js
18module.exports = ({ env }) => ({
19 upload: { provider: 'local' },
20 email: { provider: 'sendgrid' },
21});3. Maintenir une sécurité stricte des types
Tout code nouveau ou mis à jour doit inclure des types TypeScript exacts ou des définitions JSDoc. Évitez d'utiliser des types de retour manquants ou une inférence de type implicite dans les modules partagés.
❌ Exemple de non-conformité
1// src/api/user/services/user.ts
2export const createUser = (data) => {
3 return strapi.db.query('api::user.user').create({ data });
4};✅ Exemple de conformité
1// src/api/user/services/user.ts
2import { User } from './types';
3
4export const createUser = async (data: User): Promise<User> => {
5 return await strapi.db.query('api::user.user').create({ data });
6};4. Dénomination cohérente des services et des contrôleurs
Les noms des contrôleurs et des services doivent correspondre clairement à leur domaine (par exemple, user.controller.js avec user.service.js).
❌ Exemple de non-conformité
1src/
2└── api/
3 └── user/
4 ├── controllers/
5 │ └── mainController.js
6 ├── services/
7 │ └── accountService.js
8 ├── routes/
9 │ └── utilisateur.js✅ Exemple de conformité
1src/
2└── api/
3 └── user/
4 ├── controllers/
5 │ └── utilisateur.js
6 ├── services/
7 │ └── utilisateur.js
8 ├── routes/
9 │ └── utilisateur.js
10 └── content-types/
11 └── user/schéma.json
Règles : Qualité du code et maintenabilité
5. Simplifier le flux de contrôle grâce aux retours anticipés
Au lieu d'imbriquer des if/else profonds, renvoyez-les rapidement lorsque les conditions échouent.
❌ Exemple de non-conformité
1// src/api/article/controllers/article.js
2module.exports = {
3 async create(ctx) {
4 const { title, content, author } = ctx.request.body;
5
6 if (title) {
7 if (content) {
8 if (author) {
9 const article = await strapi.db.query('api::article.article').create({
10 data: { title, content, author },
11 });
12 ctx.body = article;
13 } else {
14 ctx.throw(400, 'Missing author');
15 }
16 } else {
17 ctx.throw(400, 'Missing content');
18 }
19 } else {
20 ctx.throw(400, 'Missing title');
21 }
22 },
23};✅ Exemple de conformité
1// src/api/article/controllers/article.js
2module.exports = {
3 async create(ctx) {
4 const { title, content, author } = ctx.request.body;
5
6 if (!title) ctx.throw(400, 'Missing title');
7 if (!content) ctx.throw(400, 'Missing content');
8 if (!author) ctx.throw(400, 'Missing author');
9
10 const article = await strapi.db.query('api::article.article').create({
11 data: { title, content, author },
12 });
13
14 ctx.body = article;
15 },
16};6. Éviter l'imbrication excessive dans les contrôleurs
Évitez les grands blocs de logique imbriquée à l'intérieur des contrôleurs ou des services. Extraire les conditions répétées ou complexes dans des fonctions d'aide ou des utilitaires bien nommés.
❌ Exemple de non-conformité
1// src/api/order/controllers/order.js
2module.exports = {
3 async create(ctx) {
4 const { items, user } = ctx.request.body;
5
6 if (user && user.role === 'customer') {
7 if (items && items.length > 0) {
8 const stock = await strapi.service('api::inventory.inventory').checkStock(items);
9 if (stock.every((i) => i.available)) {
10 const order = await strapi.db.query('api::order.order').create({ data: { items, user } });
11 ctx.body = order;
12 } else {
13 ctx.throw(400, 'Some items are out of stock');
14 }
15 } else {
16 ctx.throw(400, 'No items in order');
17 }
18 } else {
19 ctx.throw(403, 'Unauthorized user');
20 }
21 },
22};✅ Exemple de conformité
1// src/api/order/utils/validation.js
2const isCustomer = (user) => user?.role === 'customer';
3const hasItems = (items) => Array.isArray(items) && items.length > 0;
4
5// src/api/order/controllers/order.js
6module.exports = {
7 async create(ctx) {
8 const { items, user } = ctx.request.body;
9
10 if (!isCustomer(user)) ctx.throw(403, 'Unauthorized user');
11 if (!hasItems(items)) ctx.throw(400, 'No items in order');
12
13 const stock = await strapi.service('api::inventory.inventory').checkStock(items);
14 const allAvailable = stock.every((i) => i.available);
15 if (!allAvailable) ctx.throw(400, 'Some items are out of stock');
16
17 const order = await strapi.db.query('api::order.order').create({ data: { items, user } });
18 ctx.body = order;
19 },
20};7. Garder la logique métier en dehors des contrôleurs
Les contrôleurs doivent rester légers et se contenter d'orchestrer les demandes. Déplacer la logique d'entreprise vers les services.
❌ Exemple de non-conformité
1// src/api/article/controllers/article.js
2module.exports = {
3 async create(ctx) {
4 const { title, content, authorId } = ctx.request.body;
5
6 const author = await strapi.db.query('api::author.author').findOne({ where: { id: authorId } });
7 if (!author) ctx.throw(400, 'Author not found');
8
9 const timestamp = new Date().toISOString();
10 const slug = title.toLowerCase().replace(/\s+/g, '-');
11
12 const article = await strapi.db.query('api::article.article').create({
13 data: { title, content, slug, publishedAt: timestamp, author },
14 });
15
16 await strapi.plugins['email'].services.email.send({
17 to: author.email,
18 subject: `New article: ${title}`,
19 html: `<p>${content}</p>`,
20 });
21
22 ctx.body = article;
23 },
24};✅ Exemple de conformité
1// src/api/article/controllers/article.js
2module.exports = {
3 async create(ctx) {
4 const article = await strapi.service('api::article.article').createArticle(ctx.request.body);
5 ctx.body = article;
6 },
7};// src/api/article/services/article.js
module.exports = ({ strapi }) => ({
async createArticle(data) {
const { title, content, authorId } = data;
const author = await strapi.db.query('api::author.author').findOne({ where: { id: authorId } });
if (!author) throw new Error('Author not found');
const slug = title.toLowerCase().replace(/\s+/g, '-');
const article = await strapi.db.query('api::article.article').create({
data: { title, content, slug, author },
});
await strapi.plugins['email'].services.email.send({
to: author.email,
subject: `New article: ${title}`,
html: `<p>${content}</p>`,
});
return article;
},
});8. Utiliser les fonctions d'utilité pour les modèles répétés
Les modèles dupliqués (par exemple, validation, formatage) devraient se trouver dans des utilitaires partagés.
❌ Exemple de non-conformité
// src/api/article/controllers/article.js
module.exports = {
async create(ctx) {
const { title } = ctx.request.body;
const slug = title.toLowerCase().replace(/\s+/g, '-');
ctx.body = await strapi.db.query('api::article.article').create({ data: { ...ctx.request.body, slug } });
},
};
// src/api/event/controllers/event.js
module.exports = {
async create(ctx) {
const { name } = ctx.request.body;
const slug = name.toLowerCase().replace(/\s+/g, '-');
ctx.body = await strapi.db.query('api::event.event').create({ data: { ...ctx.request.body, slug } });
},
};✅ Exemple de conformité
// src/utils/slugify.js
module.exports = (texte) => text.toLowerCase().trim().replace(/\s+/g, '-') ;// src/api/article/controllers/article.js
const slugify = require('../../../utils/slugify');
module.exports = {
async create(ctx) {
const { title } = ctx.request.body;
const slug = slugify(title);
ctx.body = await strapi.db.query('api::article.article').create({ data: { ...ctx.request.body, slug } });
},
};9. Supprimer les journaux de débogage avant la mise en production
N'utilisez pas console.log, console.warn ou console.error dans le code de production. Utilisez toujours strapi.log ou un logger configuré pour vous assurer que les logs respectent les paramètres de l'environnement et évitent d'exposer des informations sensibles.
❌ Exemple de non-conformité
// src/api/user/controllers/user.js
module.exports = {
async find(ctx) {
console.log('Request received:', ctx.request.body); // Unsafe in production
const users = await strapi.db.query('api::user.user').findMany();
console.log('Users fetched:', users.length);
ctx.body = users;
},
};✅ Exemple de conformité
// src/api/user/controllers/user.js
module.exports = {
async find(ctx) {
strapi.log.info(`Fetching users for request from ${ctx.state.user?.email || 'anonymous'}`);
const users = await strapi.db.query('api::user.user').findMany();
strapi.log.debug(`Number of users fetched: ${users.length}`);
ctx.body = users;
},
};if (process.env.NODE_ENV === 'development') {
strapi.log.debug('Request body:', ctx.request.body);
}
Règles : Pratiques en matière de bases de données et de requêtes
10. Éviter les requêtes SQL brutes
N'exécutez pas de requêtes SQL brutes dans les contrôleurs ou les services. Utilisez toujours une méthode de requête cohérente et de haut niveau (telle qu'un ORM ou un générateur de requêtes) pour garantir la maintenabilité, appliquer les règles et les crochets, et réduire les risques de sécurité.
❌ Exemple de non-conformité
// src/api/user/services/user.js
module.exports = {
async findActiveUsers() {
const knex = strapi.db.connection;
const result = await knex.raw('SELECT * FROM users WHERE active = true'); // Raw SQL
return result.rows;
},
};✅ Exemple de conformité
// src/api/user/services/user.js
module.exports = {
async findActiveUsers() {
return await strapi.db.query('api::user.user').findMany({
where: { active: true },
});
},
};11. Utiliser le moteur de recherche de Strapi de manière cohérente
Ne mélangez pas différentes méthodes d'accès à la base de données (par exemple, appels ORM ou requêtes brutes) au sein d'une même fonctionnalité. Utilisez une approche de requête unique et cohérente pour garantir la maintenabilité, la lisibilité et un comportement prévisible.
❌ Exemple de non-conformité
// src/api/order/services/order.js
module.exports = {
async getPendingOrders() {
// Using entityService
const orders = await strapi.entityService.findMany('api::order.order', {
filters: { status: 'pending' },
});
// Mixing with raw db query
const rawOrders = await strapi.db.connection.raw('SELECT * FROM orders WHERE status = "pending"');
return { orders, rawOrders };
},
};✅ Exemple de conformité
// src/api/order/services/order.js
module.exports = {
async getPendingOrders() {
return await strapi.db.query('api::order.order').findMany({
where: { status: 'pending' },
});
},
};12. Optimiser les appels à la base de données
Regroupez les requêtes de base de données connexes ou combinez-les en une seule opération pour éviter les goulets d'étranglement au niveau des performances et réduire les appels séquentiels inutiles.
❌ Exemple de non-conformité
async function getArticlesWithAuthors() {
const articles = await db.query('articles').findMany();
// Fetch author for each article sequentially
for (const article of articles) {
article.author = await db.query('authors').findOne({ id: article.authorId });
}
return articles;
}✅ Exemple de conformité
async function getArticlesWithAuthors() {
return await db.query('articles').findMany({ populate: ['author'] });
}
Règles : API et sécurité
13. Valider les entrées avec les validateurs Strapi
Validez toutes les données entrantes à l'aide d'un mécanisme de validation cohérent avant de les utiliser dans des contrôleurs, des services ou des opérations de base de données.
❌ Exemple de non-conformité
async function createUser(req, res) {
const { username, email } = req.body;
// Directly inserting into database without validation
const user = await db.query('users').create({ username, email });
res.send(user);
}✅ Exemple de conformité
const Joi = require('joi');
async function createUser(req, res) {
const schema = Joi.object({
username: Joi.string().min(3).required(),
email: Joi.string().email().required(),
});
const { error, value } = schema.validate(req.body);
if (error) return res.status(400).send(error.details);
const user = await db.query('users').create(value);
res.send(user);
}14. Assainir les données de l'utilisateur avant de les enregistrer
Assainir toutes les entrées avant de les enregistrer dans la base de données ou de les transmettre à d'autres systèmes.
❌ Exemple de non-conformité
async function createComment(req, res) {
const { text, postId } = req.body;
// Directly saving data
const comment = await db.query('comments').create({ text, postId });
res.send(comment);
}✅ Exemple de conformité
const sanitizeHtml = require('sanitize-html');
async function createComment(req, res) {
const { text, postId } = req.body;
const sanitizedText = sanitizeHtml(text, { allowedTags: [], allowedAttributes: {} });
const comment = await db.query('comments').create({ text: sanitizedText, postId });
res.send(comment);
}15. Contrôler les autorisations
Appliquer des contrôles d'autorisation sur chaque itinéraire protégé pour s'assurer que seuls les utilisateurs autorisés peuvent y accéder.
❌ Exemple de non-conformité
async function deleteUser(req, res) {
const { userId } = req.params;
// No check for admin or owner
await db.query('users').delete({ id: userId });
res.send({ success: true });
}✅ Exemple de conformité
async function deleteUser(req, res) {
const { userId } = req.params;
const requestingUser = req.user;
// Allow only admins or the owner
if (!requestingUser.isAdmin && requestingUser.id !== userId) {
return res.status(403).send({ error: 'Forbidden' });
}
await db.query('users').delete({ id: userId });
res.send({ success: true });
}16. Gestion cohérente des erreurs avec Boom
Traiter les erreurs de manière cohérente dans tous les itinéraires de l'API à l'aide d'un mécanisme de traitement des erreurs centralisé ou unifié.
❌ Exemple de non-conformité
async function getUser(req, res) {
const { id } = req.params;
try {
const user = await db.query('users').findOne({ id });
if (!user) res.status(404).send('User not found'); // raw string error
else res.send(user);
} catch (err) {
res.status(500).send(err.message); // different error format
}
}✅ Exemple de conformité
const { createError } = require('../utils/errors');
async function getUser(req, res, next) {
try {
const { id } = req.params;
const user = await db.query('users').findOne({ id });
if (!user) throw createError(404, 'User not found');
res.send(user);
} catch (err) {
next(err); // passes error to centralized error handler
}
}// src/utils/errors.js
function createError(status, message) {
return { status, message };
}
function errorHandler(err, req, res, next) {
res.status(err.status || 500).json({ error: err.message });
}
module.exports = { createError, errorHandler };
Règles : Essais et documentation
17. Ajouter ou mettre à jour les tests pour chaque fonctionnalité
Un nouveau code sans tests ne sera pas fusionné, les tests font partie de la définition de "done".
❌ Exemple de non-conformité
// src/api/user/services/user.js
module.exports = {
async createUser(data) {
const user = await db.query('users').create(data);
return user;
},
};
// No test file exists for this service✅ Exemple de conformité
// tests/user.service.test.js
const { createUser } = require('../../src/api/user/services/user');
describe('User Service', () => {
it('should create a new user', async () => {
const mockData = { username: 'testuser', email: 'test@example.com' };
const result = await createUser(mockData);
expect(result).toHaveProperty('id');
expect(result.username).toBe('testuser');
expect(result.email).toBe('test@example.com');
});
});18. Documenter les nouveaux critères d'évaluation
Chaque ajout d'API doit être documenté dans la documentation de référence avant la fusion.
❌ Exemple de non-conformité
// src/api/user/controllers/user.js
module.exports = {
async deactivate(ctx) {
const { userId } = ctx.request.body;
await db.query('users').update({ id: userId, active: false });
ctx.body = { success: true };
},
};
// No update in API reference or docs✅ Exemple de conformité
// src/api/user/controllers/user.js
module.exports = {
/**
* Deactivate a user account.
* POST /users/deactivate
* Body: { userId: string }
* Response: { success: boolean }
* Errors: 400 if userId missing, 404 if user not found
*/
async deactivate(ctx) {
const { userId } = ctx.request.body;
if (!userId) ctx.throw(400, 'userId is required');
const user = await db.query('users').findOne({ id: userId });
if (!user) ctx.throw(404, 'User not found');
await db.query('users').update({ id: userId, active: false });
ctx.body = { success: true };
},
};Référence Docs Mise à jour Exemple :
### POST /users/deactivate
**Request Body:**
```json
{
"userId": "string"
}Réponse :
{
"success": true
}Erreurs :
- 400 : l'identifiant est obligatoire
- 404 : Utilisateur non trouvé
Pourquoi cela fonctionne-t-il ?
- Les développeurs et les consommateurs d'API peuvent découvrir et utiliser les points de terminaison de manière fiable.
- Assure la cohérence entre l'implémentation et la documentation
- Facilite la maintenance et l'intégration
---
Voulez-vous que je continue avec la **règlen°19 ("Utiliser JSDoc pour les utilitaires partagés")** dans le même format ?19. Utiliser JSDoc pour les utilitaires partagés
Les fonctions partagées doivent être expliquées à l'aide de JSDoc afin de faciliter l'intégration et la collaboration.
❌ Exemple de non-conformité
// src/utils/slugify.js
function slugify(text) {
return text.toLowerCase().trim().replace(/\s+/g, '-');
}
module.exports = slugify;✅ Exemple de conformité
// src/utils/slugify.js
/**
* Converts a string into a URL-friendly slug.
*
* @param {string} text - The input string to convert.
* @returns {string} A lowercased, trimmed, dash-separated slug.
*/
function slugify(text) {
return text.toLowerCase().trim().replace(/\s+/g, '-');
}
module.exports = slugify;20. Mettre à jour le changelog avec chaque PR significatif
Mettez à jour le journal des modifications du projet avec chaque fonctionnalité significative, correction de bogue ou changement d'API avant de fusionner un PR.
❌ Exemple de non-conformité
# CHANGELOG.md
## [1.0.0] - 2025-09-01
- Version initiale✅ Exemple de conformité
# CHANGELOG.md
## [1.1.0] - 2025-10-06
- Ajout d'un point de terminaison pour la désactivation des utilisateurs (`POST /users/deactivate`)
- Correction d'un bug dans la génération de slugs pour les titres d'articles
- Mise à jour du service de notification par courriel pour gérer les envois par lotsConclusion
Nous avons étudié le référentiel public de Strapi pour comprendre comment des modèles de code cohérents permettent à de grands projets open-source de se développer sans perdre en qualité. Ces 20 règles ne sont pas de la théorie. Ce sont des leçons pratiques tirées directement de la base de code de Strapi qui rendent le projet plus facile à maintenir, plus sûr et plus facile à lire.
Si votre projet est en pleine croissance, appliquez ces leçons à vos revues de code. Elles vous aideront à passer moins de temps à nettoyer du code désordonné et plus de temps à développer des fonctionnalités qui comptent vraiment.
.avif)
