
Introduction
Strapi est l'une des plateformes CMS headless open-source les plus populaires, mais c'est aussi une immense base de code avec des centaines de contributeurs et des milliers de pull requests. Maintenir la qualité d'un projet d'une telle envergure n'est pas facile. Cela nécessite des règles de revue de code claires et cohérentes pour garantir que chaque contribution reste fiable, lisible et sécurisée.
Dans cet article, nous avons compilé un ensemble de règles de revue de code basées sur le dépôt public de Strapi. Ces règles proviennent de travaux réels : des problèmes concrets, des discussions et des pull requests qui ont aidé le projet à se développer tout en maintenant la stabilité de la base de code.
Pourquoi il est difficile de maintenir la qualité du code dans un grand projet open source
Maintenir 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, des bénévoles aux ingénieurs expérimentés, soumettent des pull requests, chacune introduisant de nouvelles fonctionnalités, des corrections de bugs ou des refactorisations. Sans règles claires, la base de code peut rapidement devenir incohérente, fragile ou difficile à naviguer.
Parmi les principaux défis, on trouve :
- Contributeurs diversifiés avec des niveaux d'expérience variés.
- Modèles de codage incohérents entre les modules.
- Bugs cachés et logique dupliquée qui s'insinuent.
- Risques de sécurité si les processus ne sont pas appliqués.
- Revues chronophages pour les volontaires peu familiers avec l'ensemble du codebase.
Pour relever ces défis, les projets réussis s'appuient sur des processus structurés : des standards partagés, un outillage automatisé et des directives claires. Ces pratiques garantissent la maintenabilité, la lisibilité et la sécurité, même lorsque le projet prend de l'ampleur et attire davantage de contributeurs.
Comment le respect de ces règles améliore la maintenabilité, la sécurité et l'intégration
Le respect d'un ensemble clair de règles de revue de code a un impact direct sur la santé de votre projet :
- Maintenabilité : Des structures de dossiers, des conventions de nommage et des modèles de codage cohérents facilitent la lecture, la navigation et l'extension de la base de code.
- Sécurité : La validation des entrées, la désinfection, les vérifications de permissions et l'accès contrôlé aux bases de données réduisent les vulnérabilités et préviennent les fuites de données accidentelles.
- Onboarding plus rapide : Des normes partagées, des utilitaires documentés et des exemples clairs aident les nouveaux contributeurs à comprendre rapidement le projet et à 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 à mesure que le nombre de contributeurs augmente.
Relier le contexte aux règles
Avant d'examiner les règles, il est important de comprendre que maintenir une qualité de code élevée dans un projet tel que 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 les défis réels qui apparaissent dans la base de code de Strapi.
Les exemples fournis pour chaque règle illustrent des approches non conformes et conformes, offrant une image claire de la manière dont ces principes s'appliquent en 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 standards de configuration.
Règles : Structure et cohérence du projet
1. Suivre les conventions de dossiers établies de Strapi
Évitez de disperser les fichiers ou d'inventer de nouvelles structures. Respectez l'organisation de projet établie de Strapi pour maintenir une navigation prévisible.
❌ Exemple non conforme
1src/
2├──controllers/
3│└── userController.js
4├──services/
5│└── userLogic.js
6├──routes/
7│└── userRoutes.js
8└──utils/
9 └─── helper.js✅ Exemple conforme
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 les mêmes conventions de structure, de nommage et de formatage dans tous les fichiers de configuration pour assurer la cohérence et prévenir les erreurs.
❌ Exemple non conforme
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 conforme
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 stricte sécurité de type
Tout code nouveau ou mis à jour doit inclure des types TypeScript précis ou des définitions JSDoc. Évitez d'utiliser `any`, les types de retour manquants ou l'inférence de type implicite dans les modules partagés.
❌ Exemple non conforme
1// src/api/user/services/user.ts
2export const createUser = (data) => {
3 return strapi.db.query('api::user.user').create({ data });
4};✅ Exemple conforme
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. Nomenclature cohérente pour les services et les contrôleurs
Les noms des contrôleurs et des services doivent clairement correspondre à leur domaine (par exemple, user.controller.js avec user.service.js).
❌ Exemple non conforme
1src/
2└── api/
3 └── user/
4 ├── controllers/
5 │ └── mainController.js
6 ├── services/
7 │ └── accountService.js
8 ├── routes/
9 │ └── utilisateur.js✅ Exemple conforme
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é et maintenabilité du code
5. Simplifier le flux de contrôle avec des retours anticipés
Au lieu d'imbrications profondes de if/else, retournez tôt lorsque les conditions échouent.
❌ Exemple non conforme
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 conforme
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 dans les contrôleurs ou les services. Extrayez les conditions répétées ou complexes dans des fonctions d'aide ou des utilitaires bien nommés.
❌ Exemple non conforme
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 conforme
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. Maintenir la logique métier hors des contrôleurs
Les contrôleurs doivent rester légers et uniquement orchestrer les requêtes. Déplacez la logique métier vers les services.
❌ Exemple non conforme
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 conforme
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 des fonctions utilitaires pour les motifs récurrents
Les motifs dupliqués (par exemple, validation, formatage) devraient résider dans des utilitaires partagés.
❌ Exemple non conforme
// 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 conforme
// 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 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 d'environnement et évitent d'exposer des informations sensibles.
❌ Exemple non conforme
// 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 conforme
// 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 : Bases de données et pratiques de requête
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 (comme un ORM ou un constructeur de requêtes) pour assurer la maintenabilité, appliquer les règles/hooks et réduire les risques de sécurité.
❌ Exemple non conforme
// 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 conforme
// 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 requêtes de Strapi de manière cohérente
Ne mélangez pas différentes méthodes d'accès aux bases de données (par exemple, appels ORM vs requêtes brutes) au sein de la même fonctionnalité. Utilisez une approche de requête unique et cohérente pour assurer la maintenabilité, la lisibilité et un comportement prévisible.
❌ Exemple non conforme
// 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 conforme
// 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 de base de données
Regrouper les requêtes de base de données connexes ou les combiner en une seule opération pour éviter les goulots d'étranglement de performance et réduire les appels séquentiels inutiles.
❌ Exemple non conforme
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 conforme
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
Ne faites jamais confiance aux entrées provenant de clients ou de sources externes. Validez toutes les données entrantes à l'aide d'un mécanisme de validation cohérent avant de les utiliser dans les contrôleurs, les services ou les opérations de base de données.
❌ Exemple non conforme
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 conforme
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. Nettoyer les entrées utilisateur avant de les enregistrer
Assainissez toutes les entrées avant de les enregistrer dans la base de données ou de les transmettre à d'autres systèmes.
❌ Exemple non conforme
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 conforme
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. Appliquer les vérifications de permissions
Appliquez des vérifications de permissions sur chaque route protégée pour garantir que seuls les utilisateurs autorisés peuvent y accéder.
❌ Exemple non conforme
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 conforme
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
Gérer les erreurs de manière cohérente sur toutes les routes d'API en utilisant un mécanisme de gestion des erreurs centralisé ou unifié.
❌ Exemple non conforme
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 conforme
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 : Tests et documentation
17. Ajouter ou mettre à jour des tests pour chaque fonctionnalité
Le nouveau code sans tests ne sera pas fusionné, les tests font partie de la définition de « terminé ».
❌ Exemple non conforme
// 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 conforme
// 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 endpoints
Tout ajout d'API doit être documenté dans la documentation de référence avant la fusion.
❌ Exemple non conforme
// 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 conforme
// 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 };
},
};Exemple de mise à jour des documents de référence :
### POST /users/deactivate
**Request Body:**
```json
{
"userId": "string"
}Réponse :
{
"success": true
}Erreurs :
- 400 : userId est requis
- 404 : Utilisateur introuvable
Pourquoi cela fonctionne :
- Les développeurs et les consommateurs d'API peuvent découvrir et utiliser les endpoints 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ègle n°19 (« Utiliser JSDoc pour les utilitaires partagés »)** dans le même format ensuite ?19. Utiliser JSDoc pour les utilitaires partagés
Les fonctions partagées devraient être expliquées avec JSDoc pour faciliter l'intégration et la collaboration.
❌ Exemple non conforme
// src/utils/slugify.js
function slugify(text) {
return text.toLowerCase().trim().replace(/\s+/g, '-');
}
module.exports = slugify;✅ Exemple conforme
// 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 à chaque PR significative
Mettez à jour le changelog du projet avec chaque fonctionnalité majeure, correction de bug ou modification d'API avant de fusionner une PR.
❌ Exemple non conforme
# CHANGELOG.md
## [1.0.0] - 2025-09-01
- Version initiale✅ Exemple conforme
# CHANGELOG.md
## [1.1.0] - 2025-10-06
- Ajout du point de terminaison de désactivation d'utilisateur (`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 e-mail pour gérer l'envoi par lotsConclusion
Nous avons étudié le dépôt public de Strapi pour comprendre comment des modèles de code cohérents aident les grands projets open source à se développer sans perdre en qualité. Ces 20 règles ne sont pas théoriques. 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écurisé et plus lisible.
Si votre projet prend de l'ampleur, retenez ces leçons et appliquez-les à 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)
