
Introduzione
Strapi è una delle piattaforme headless CMS open source più popolari, ma è anche un progetto di grandi dimensioni con centinaia di contributor e migliaia di pull request. Mantenere elevata la qualità di un progetto così vasto non è semplice. Sono necessarie regole di code review chiare e coerenti per garantire che ogni contributo rimanga affidabile, leggibile e sicuro.
In questo articolo abbiamo raccolto una serie di regole di code review basate sul repository pubblico di Strapi. Queste regole derivano da casi reali: problemi concreti, discussioni e pull request che hanno aiutato il progetto a crescere mantenendo la codebase stabile.
Perché mantenere la qualità del codice in un grande progetto open source è difficile
Mantenere la qualità in un grande progetto open source è una sfida a causa delle dimensioni e della varietà dei contributi. Centinaia o persino migliaia di sviluppatori, dai volontari agli ingegneri più esperti, inviano pull request introducendo nuove funzionalità, correzioni di bug o refactoring. Senza regole chiare, la codebase può rapidamente diventare incoerente, fragile o difficile da esplorare.
Tra le sfide principali troviamo:
- Contributor differenti con livelli di esperienza variabili.
- Pattern di codice incoerenti tra i diversi moduli.
- Bug nascosti e logica duplicata che si insinuano nel codice.
- Rischi per la sicurezza qualora le procedure non vengano applicate.
- Revisioni che richiedono molto tempo per i volontari che non conoscono a fondo l'intera codebase.
Per affrontare queste sfide, i progetti di successo si affidano a processi strutturati: standard condivisi, strumenti automatizzati e linee guida chiare. Queste pratiche garantiscono manutenibilità, leggibilità e sicurezza anche quando il progetto cresce e attrae un numero maggiore di contributor.
In che modo l'adozione di queste regole migliora la manutenibilità, la sicurezza e l'onboarding
L'applicazione di una serie chiara di regole di code review ha un impatto diretto sulla salute del tuo progetto:
- Manutenibilità: strutture di cartelle coerenti, convenzioni di denominazione e pattern di codifica facilitano la lettura, la navigazione e l'estensione della codebase.
- Sicurezza: la validazione e la sanitizzazione degli input, i controlli sui permessi e l'accesso controllato al database riducono le vulnerabilità e prevengono fughe di dati accidentali.
- Onboarding più rapido: standard condivisi, utility documentate ed esempi chiari aiutano i nuovi contributori a comprendere rapidamente il progetto e a contribuire con sicurezza.
Applicando queste regole, i team possono assicurarsi che la codebase rimanga scalabile, affidabile e sicura, anche con la crescita del numero di contributori.
Collegare il contesto alle regole
Prima di esaminare le regole, è importante comprendere che mantenere alta la qualità del codice in un progetto come Strapi non significa solo seguire le best practice generali. Significa disporre di pattern e standard chiari che aiutino centinaia di contributori a remare nella stessa direzione. Ognuna delle 20 regole seguenti si concentra su sfide reali che emergono nella codebase di Strapi.
Gli esempi forniti per ciascuna regola illustrano sia gli approcci non conformi che quelli conformi, offrendo un quadro chiaro di come questi principi si applichino nella pratica.
Ora esploriamo le regole che rendono la codebase di Strapi scalabile, coerente e di alta qualità, iniziando dalla struttura del progetto e dagli standard di configurazione.
Regole: struttura del progetto e coerenza
1. Segui le convenzioni sulle cartelle stabilite da Strapi
Evita di spargere file o di inventare nuove strutture. Attieniti al layout di progetto stabilito da Strapi per mantenere la navigazione prevedibile.
❌ Esempio non conforme
1src/
2├── controllers/
3│ └── userController.js
4├── services/
5│ └── userLogic.js
6├── routes/
7│ └── userRoutes.js
8└── utils/
9 └── helper.js✅ Esempio 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. Mantieni coerenti i file di configurazione
Usa la stessa struttura, denominazione e convenzioni di formattazione in tutti i file di configurazione per garantire coerenza e prevenire errori.
❌ Esempio 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});✅ Esempio 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. Mantieni una rigida type safety
Tutto il codice nuovo o aggiornato deve includere tipi TypeScript accurati o definizioni JSDoc. Evita di usare any, tipi di ritorno mancanti o l'inferenza implicita dei tipi nei moduli condivisi.
❌ Esempio non conforme
1// src/api/user/services/user.ts
2export const createUser = (data) => {
3 return strapi.db.query('api::user.user').create({ data });
4};✅ Esempio 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. Denominazione coerente per services e controllers
I nomi di controller e service devono corrispondere chiaramente al loro dominio (ad es., user.controller.js con user.service.js).
❌ Esempio non conforme
1src/
2└── api/
3 └── user/
4 ├── controllers/
5 │ └── mainController.js
6 ├── services/
7 │ └── accountService.js
8 ├── routes/
9 │ └── user.js✅ Esempio 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.json
Regole: Qualità del codice e manutenibilità
5. Semplifica il flusso di controllo con gli early return
Invece di annidamenti complessi di if/else, esci in anticipo quando le condizioni non sono soddisfatte.
❌ Esempio 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};✅ Esempio 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. Evita un annidamento eccessivo nei controller
Evita grandi blocchi di logica annidata all'interno di controller o servizi. Estrai le condizioni ripetute o complesse in funzioni di supporto o utility ben denominate.
❌ Esempio 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};✅ Esempio 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. Mantieni la business logic fuori dai controller
I controller dovrebbero rimanere leggeri e limitarsi a orchestrare le richieste. Sposta la business logic nei servizi.
❌ Esempio 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};✅ Esempio 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. Usa funzioni di utility per i pattern ripetuti
I pattern duplicati (ad es. validazione, formattazione) dovrebbero trovarsi in utility condivise.
❌ Esempio 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 } });
},
};✅ Esempio conforme
// src/utils/slugify.js
module.exports = (text) => 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. Rimuovi i log di debug prima della produzione
Non usare console.log, console.warn o console.error nel codice di produzione. Usa sempre strapi.log o un logger configurato per assicurarti che i log rispettino le impostazioni dell'ambiente ed evitare l'esposizione di informazioni sensibili.
❌ Esempio 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;
},
};✅ Esempio 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);
}
Regole: Database e pratiche per le query
10. Evita query SQL grezze
Non eseguire query SQL grezze nei controller o nei servizi. Usa sempre un metodo di query coerente e di alto livello (come un ORM o un query builder) per garantire la manutenibilità, applicare regole/hook e ridurre i rischi per la sicurezza.
❌ Esempio 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;
},
};✅ Esempio conforme
// src/api/user/services/user.js
module.exports = {
async findActiveUsers() {
return await strapi.db.query('api::user.user').findMany({
where: { active: true },
});
},
};11. Usa in modo coerente il motore di query di Strapi
Non mischiare diversi metodi di accesso al database (ad es. chiamate ORM rispetto a query grezze) all'interno della stessa funzionalità. Usa un approccio di query singolo e coerente per garantire manutenibilità, leggibilità e comportamento prevedibile.
❌ Esempio 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 };
},
};✅ Esempio conforme
// src/api/order/services/order.js
module.exports = {
async getPendingOrders() {
return await strapi.db.query('api::order.order').findMany({
where: { status: 'pending' },
});
},
};12. Ottimizza le chiamate al database
Raggruppa le query correlate al database in batch o combinale in un'unica operazione per prevenire colli di bottiglia nelle prestazioni e ridurre chiamate sequenziali non necassarie.
❌ Esempio 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;
}✅ Esempio conforme
async function getArticlesWithAuthors() {
return await db.query('articles').findMany({ populate: ['author'] });
}
Regole: API e Sicurezza
13. Valida l'input con i validatori di Strapi
Non fidarti mai degli input provenienti da client o fonti esterne. Valida tutti i dati in arrivo utilizzando un meccanismo di validazione coerente prima di utilizzarli in controller, servizi o operazioni sul database.
❌ Esempio 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);
}✅ Esempio 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. Sanitizza l'input utente prima del salvataggio
Sanitizza tutti gli input prima di salvarli nel database o di passarli ad altri sistemi.
❌ Esempio 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);
}✅ Esempio 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. Applica i controlli dei permessi
Applica controlli dei permessi su ogni rotta protetta per assicurarti che solo gli utenti autorizzati possano accedervi.
❌ Esempio 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 });
}✅ Esempio 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. Gestione coerente degli errori con Boom
Gestisci gli errori in modo coerente in tutte le rotte API utilizzando un meccanismo di gestione degli errori centralizzato o unificato.
❌ Esempio 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
}
}✅ Esempio 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 };
Regole: Test e documentazione
17. Aggiungi o aggiorna i test per ogni funzionalità
Il nuovo codice privo di test non verrà unito; i test fanno parte della definizione di completamento (definition of done).
❌ Esempio 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✅ Esempio 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. Documenta i nuovi endpoint
Ogni aggiunta di API deve essere documentata nella documentazione di riferimento prima dell'unione.
❌ Esempio 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✅ Esempio 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 };
},
};Esempio di aggiornamento della documentazione di riferimento:
### POST /users/deactivate
**Request Body:**
```json
{
"userId": "string"
}Risposta:
{
"success": true
}Errori:
- 400: userId è obbligatorio
- 404: Utente non trovato
Perché funziona:
- Gli sviluppatori e i consumatori di API possono scoprire e utilizzare gli endpoint in modo affidabile
- Garantisce la coerenza tra implementazione e documentazione
- Semplifica la manutenzione e l'onboarding
---
Vuoi che continui con la **Regola n. 19 ("Usa JSDoc per le utility condivise")** nello stesso formato?19. Usa JSDoc per le utility condivise
Le funzioni condivise dovrebbero essere spiegate con JSDoc per facilitare l'onboarding e la collaborazione.
❌ Esempio non conforme
// src/utils/slugify.js
function slugify(text) {
return text.toLowerCase().trim().replace(/\s+/g, '-');
}
module.exports = slugify;✅ Esempio 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. Aggiorna il changelog con ogni PR significativa
Aggiorna il changelog del progetto con ogni funzionalità rilevante, correzione di bug o modifica delle API prima di unire una PR.
❌ Esempio non conforme
# CHANGELOG.md
## [1.0.0] - 2025-09-01
- Rilascio iniziale✅ Esempio conforme
# CHANGELOG.md
## [1.1.0] - 2025-10-06
- Aggiunto endpoint di disattivazione utente (`POST /users/deactivate`)
- Risolto un bug nella generazione dello slug per i titoli degli articoli
- Aggiornato il servizio di notifica email per gestire l'invio in batchConclusione
Abbiamo analizzato il repository pubblico di Strapi per capire in che modo pattern di codice coerenti aiutino i grandi progetti open source a crescere senza perdere qualità. Queste 20 regole non sono teoria. Sono lezioni pratiche tratte direttamente dalla base di codice di Strapi che rendono il progetto più facile da mantenere, più sicuro e più leggibile.
Se il tuo progetto sta crescendo, prendi queste lezioni e applicale alle tue code review. Ti aiuteranno a dedicare meno tempo alla pulizia del codice disordinato e più tempo alla creazione di funzionalità che contano davvero.

