
Introduzione
Strapi è una delle piattaforme CMS headless open source più popolari, ma è anche un codice sorgente di enormi dimensioni, con centinaia di collaboratori e migliaia di pull request. Mantenere alta la qualità di un progetto così vasto non è facile. Occorrono regole di revisione del codice chiare e coerenti per garantire che ogni contributo rimanga affidabile, leggibile e sicuro.
In questo articolo abbiamo raccolto una serie di regole per la revisione del codice basate sul repository pubblico di Strapi. Queste regole nascono dall’esperienza pratica: problemi reali, discussioni e pull request che hanno contribuito alla crescita del progetto, mantenendo al contempo stabile il codice.
Perché è difficile garantire la qualità del codice in un grande progetto open source
Mantenere la qualità in un grande progetto open source è una sfida, a causa delle enormi dimensioni e della grande varietà dei contributi. Centinaia o addirittura migliaia di sviluppatori, dai volontari agli ingegneri esperti, inviano pull request, ognuna delle quali introduce nuove funzionalità, correzioni di bug o rifattorizzazioni. Senza regole chiare, il codice può diventare rapidamente incoerente, instabile o difficile da consultare.
Tra le principali sfide figurano:
- Collaboratori di vario tipo con diversi livelli di esperienza.
- Modelli di codifica non uniformi tra i vari moduli.
- Si stanno insinuando bug nascosti e logiche duplicate.
- Rischi per la sicurezza qualora le procedure non vengano applicate.
- Revisioni che richiedono molto tempo per i volontari che non hanno familiarità con l'intero codice sorgente.
Per affrontare queste sfide, i progetti di successo si basano su processi strutturati: standard condivisi, strumenti automatizzati e linee guida chiare. Queste pratiche garantiscono la manutenibilità, la leggibilità e la sicurezza anche man mano che il progetto cresce e attira un numero sempre maggiore di collaboratori.
In che modo il rispetto di queste regole migliora la manutenibilità, la sicurezza e l'inserimento dei nuovi assunti
Il rispetto di una serie ben definita di regole per la revisione del codice ha un impatto diretto sullo stato di salute del vostro progetto:
- Manutenibilità: strutture di cartelle, convenzioni di denominazione e modelli di codifica coerenti rendono più facile la lettura, la navigazione e l'estensione del codice.
- Sicurezza: la convalida degli input, la sanificazione, i controlli delle autorizzazioni e l'accesso controllato al database riducono le vulnerabilità e prevengono la fuga accidentale di dati.
- Integrazione più rapida: standard condivisi, utilità documentate ed esempi chiari aiutano i nuovi collaboratori a comprendere rapidamente il progetto e a contribuire con sicurezza.
Applicando queste regole, i team possono garantire che il codice rimanga scalabile, affidabile e sicuro, anche con l'aumentare del numero di collaboratori.
Collegare il contesto alle regole
Prima di esaminare le regole, è importante comprendere che garantire un’elevata qualità del codice in un progetto come Strapi non significa solo seguire le migliori pratiche generali. Si tratta piuttosto di disporre di modelli e standard chiari che aiutino centinaia di collaboratori a rimanere allineati. Ciascuna delle 20 regole riportate di seguito si concentra su sfide concrete che emergono nel codice di Strapi.
Gli esempi forniti per ciascuna regola illustrano sia gli approcci non conformi che quelli conformi, offrendo un quadro chiaro di come tali principi si applichino nella pratica.
Ora vediamo quali sono le regole che rendono il codice di Strapi scalabile, coerente e di alta qualità, partendo dalla struttura del progetto e dagli standard di configurazione.
Regole: Struttura e coerenza del progetto
1. Attenersi alle convenzioni relative alle cartelle stabilite da Strapi
Evita di disperdere i file o di inventare nuove strutture. Attieniti alla struttura di progetto standard di Strapi per garantire una navigazione intuitiva.
❌ Esempio non conforme
1src/
2├──controllers/
3│ └── userController.js
4├──servizi/
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 ├── servizi/
7 │ └── user.js
8 ├── routes/
9 │ └── user.js
10 └── tipi-di-contenuto/
11 └── user/schema.json2. Mantenere la coerenza dei file di configurazione
Utilizzare la stessa struttura, le stesse convenzioni di denominazione e di formattazione in tutti i file di configurazione per garantire la coerenza ed evitare 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. Garantire una rigorosa sicurezza dei tipi
Tutto il codice nuovo o aggiornato deve includere tipi TypeScript accurati o definizioni JSDoc. Evitare di utilizzare 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. Nomenclatura coerente per servizi e controller
I nomi dei controller e dei servizi devono corrispondere chiaramente al proprio dominio (ad esempio, user.controller.js con user.service.js).
❌ Esempio non conforme
1src/
2└── api/
3 └── utente/
4 ├── controllori/
5 │ └── controller principale.js
6 ├── servizi/
7 │ └── accountService.js
8 ├── percorsi/
9 │ └── utente.js✅ Esempio conforme
1src/
2└── api/
3 └── utente/
4 ├── controllori/
5 │ └── utente.js
6 ├── servizi/
7 │ └── utente.js
8 ├── percorsi/
9 │ └── utente.js
10 └── contenuto-tipi/
11 └── utente/schema.json
Regole: Qualità del codice e manutenibilità
5. Semplificare il flusso di controllo con i ritorni anticipati
Invece di ricorrere a un'annidamento profondo di istruzioni if/else, interrompi l'esecuzione non appena le condizioni non vengono 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. Evitare un'eccessiva annidamento nei controller
Evita di inserire grandi blocchi di logica annidata all'interno dei controller o dei servizi. Estrai le condizioni ripetitive o complesse in funzioni di supporto o utilità con nomi appropriati.
❌ 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. Evitare di inserire la logica di business nei controller
I controller devono rimanere snelli e limitarsi a coordinare le richieste. La logica di business va trasferita ai 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. Utilizzare funzioni di utilità per i modelli ricorrenti
I modelli duplicati (ad esempio, di convalida, di formattazione) dovrebbero essere inseriti nelle utilità 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
modulo.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. Rimuovere i log di debug prima di passare all'ambiente di produzione
Non utilizzare console.log, console.warn o console.error nel codice di produzione. Utilizza sempre strapi.log o un logger configurato per garantire che i log rispettino le impostazioni dell'ambiente ed evitare di divulgare 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: Pratiche relative al database e alle query
10. Evitare le query SQL non elaborati
Non eseguire query SQL "grezze" nei controller o nei servizi. Utilizza sempre un metodo di query coerente e di alto livello (come un ORM o un query builder) per garantire la manutenibilità, applicare regole e 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. Utilizzare il motore di query di Strapi in modo coerente
Non mescolare metodi di accesso al database diversi (ad esempio, chiamate ORM e query grezze) all'interno della stessa funzionalità. Utilizza un unico approccio coerente alle query per garantire la manutenibilità, la leggibilità e un 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. Ottimizzare le chiamate al database
Raggruppare le query del database o unirle in un’unica operazione per evitare colli di bottiglia nelle prestazioni e ridurre le chiamate sequenziali non necessarie.
❌ 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. Verifica i dati inseriti con i validatori di Strapi
Non fidarti mai dei dati forniti dai clienti o da fonti esterne. Verifica tutti i dati in entrata utilizzando un meccanismo di convalida coerente prima di utilizzarli nei controller, nei servizi o nelle 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. Esegui la sanificazione dei dati inseriti dall'utente prima di salvarli
Eseguire la sanificazione di tutti i dati in ingresso prima di salvarli nel database o di trasmetterli 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. Applicare i controlli dei permessi
Applicare i controlli delle autorizzazioni su ogni percorso protetto per garantire 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
Gestire gli errori in modo coerente su 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. Aggiungere o aggiornare i test per ogni funzionalità
Il nuovo codice senza test non verrà integrato; i test fanno parte della definizione di "completato".
❌ 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. Documentare i nuovi endpoint
Ogni nuova funzionalità aggiunta all'API deve essere documentata nella documentazione di riferimento prima del merge.
❌ 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 dei documenti di riferimento:
### POST /users/deactivate
**Request Body:**
```json
{
"userId": "string"
}Risposta:
{
"success": true
}Errori:
- 400: l'ID utente è obbligatorio
- 404: Utente non trovato
Perché funziona:
- Gli sviluppatori e gli utenti delle API possono individuare e utilizzare gli endpoint in modo affidabile
- Garantisce la coerenza tra l'implementazione e la documentazione
- Semplifica la manutenzione e l'onboarding
---
Vuoi che continui con la **Regola n.19 (“Utilizza JSDoc per le utilità condivise”)** nello stesso formato?19. Utilizzare JSDoc per le utilità condivise
Le funzioni condivise dovrebbero essere documentate con JSDoc per facilitare l'inserimento dei nuovi collaboratori 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. Aggiornare il log delle modifiche ad ogni pull request significativa
Prima di unire una richiesta di pull, aggiorna il log delle modifiche del progetto indicando ogni funzionalità significativa, correzione di bug o modifica alle API.
❌ Esempio non conforme
# CHANGELOG.md
## [1.0.0] - 2025-09-01
- Versione iniziale✅ Esempio conforme
# CHANGELOG.md
## [1.1.0] - 2025-10-06
- Aggiunto endpoint per la disattivazione degli utenti (`POST /users/deactivate`)
- Risolto un bug nella generazione degli slug per i titoli degli articoli
- Aggiornato il servizio di notifica via e-mail per gestire l'invio in batchConclusione
Abbiamo analizzato il repository pubblico di Strapi per capire in che modo l’adozione di modelli di codice coerenti contribuisca alla crescita dei grandi progetti open source senza comprometterne la qualità. Queste 20 regole non sono pura teoria. Si tratta di insegnamenti pratici tratti direttamente dal codice di Strapi, che rendono il progetto più facile da mantenere, più sicuro e più leggibile.
Se il tuo progetto sta crescendo, metti in pratica questi insegnamenti durante le revisioni del codice. Ti aiuteranno a dedicare meno tempo a ripulire il codice disordinato e più tempo a sviluppare funzionalità che contano davvero.

