
Introdução
Strapi é uma das plataformas CMS headless de código aberto mais populares, mas também é uma enorme base de código com centenas de contribuidores e milhares de pull requests. Manter a alta qualidade de um projeto tão grande não é fácil. Requer regras de revisão de código claras e consistentes para garantir que cada contribuição permaneça confiável, legível e segura.
Neste artigo, compilamos um conjunto de regras de revisão de código baseadas no repositório público do Strapi. Essas regras vêm de trabalho real: problemas, discussões e pull requests que ajudaram o projeto a crescer, mantendo a base de código estável.
Por que manter a qualidade do código em um grande projeto de código aberto é difícil
Manter a qualidade em um grande projeto de código aberto é desafiador devido à vasta escala e diversidade das contribuições. Centenas ou até milhares de desenvolvedores, de voluntários a engenheiros experientes, enviam pull requests, cada um introduzindo novas funcionalidades, correções de bugs ou refatorações. Sem regras claras, a base de código pode rapidamente se tornar inconsistente, frágil ou difícil de navegar.
Alguns dos principais desafios incluem:
- Colaboradores diversos com diferentes níveis de experiência.
- Padrões de codificação inconsistentes entre módulos.
- Bugs ocultos e lógica duplicada surgindo.
- Riscos de segurança se os processos não forem aplicados.
- Revisões demoradas para voluntários não familiarizados com a base de código completa.
Para enfrentar esses desafios, projetos bem-sucedidos dependem de processos estruturados: padrões compartilhados, ferramentas automatizadas e diretrizes claras. Essas práticas garantem manutenibilidade, legibilidade e segurança, mesmo à medida que o projeto cresce e atrai mais contribuidores.
Como seguir estas regras melhora a manutenibilidade, segurança e onboarding
Aderir a um conjunto claro de regras de revisão de código tem um impacto direto na saúde do seu projeto:
- Manutenibilidade: Estruturas de pastas consistentes, convenções de nomenclatura e padrões de codificação facilitam a leitura, navegação e extensão da base de código.
- Segurança: Validação de entrada, sanitização, verificações de permissão e acesso controlado ao banco de dados reduzem vulnerabilidades e previnem vazamentos acidentais de dados.
- Onboarding Mais Rápido: Padrões compartilhados, utilitários documentados e exemplos claros ajudam novos contribuidores a entender o projeto rapidamente e a contribuir com confiança.
Ao aplicar essas regras, as equipes podem garantir que a base de código permaneça escalável, confiável e segura, mesmo com o aumento do número de contribuidores.
Conectando o contexto às regras
Antes de analisar as regras, é importante entender que manter a qualidade do código alta em um projeto como o Strapi não se trata apenas de seguir as melhores práticas gerais. É sobre ter padrões e normas claras que ajudam centenas de contribuidores a estarem alinhados. Cada uma das 20 regras abaixo foca em desafios reais que surgem na base de código do Strapi.
Os exemplos fornecidos para cada regra ilustram abordagens não conformes e conformes, oferecendo uma visão clara de como esses princípios se aplicam na prática.
Agora, vamos explorar as regras que tornam a base de código do Strapi escalável, consistente e de alta qualidade, começando pela estrutura do projeto e padrões de configuração.
Regras: Estrutura e consistência do projeto
1. Siga as convenções de pastas estabelecidas da Strapi
Evite espalhar arquivos ou inventar novas estruturas. Mantenha-se fiel ao layout de projeto estabelecido do Strapi para manter a navegação previsível.
❌ Exemplo não em conformidade
1src/
2├── controllers/
3│ └── userController.js
4├── services/
5│ └── userLogic.js
6├── routes/
7│ └── userRoutes.js
8└── utils/
9 └── helper.js✅ Exemplo em conformidade
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. Mantenha os arquivos de configuração consistentes
Use as mesmas convenções de estrutura, nomenclatura e formatação em todos os arquivos de configuração para garantir consistência e prevenir erros.
❌ Exemplo não em conformidade
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});✅ Exemplo em conformidade
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. Manter segurança de tipos estrita
Todo código novo ou atualizado deve incluir tipos TypeScript precisos ou definições JSDoc. Evite usar 'any', tipos de retorno ausentes ou inferência de tipo implícita em módulos compartilhados.
❌ Exemplo não em conformidade
1// src/api/user/services/user.ts
2export const createUser = (data) => {
3 return strapi.db.query('api::user.user').create({ data });
4};✅ Exemplo em conformidade
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 consistente para serviços e controladores
Os nomes de controller e service devem corresponder claramente ao seu domínio (ex: user.controller.js com user.service.js).
❌ Exemplo não em conformidade
1src/
2└── api/
3 └── user/
4 ├── controllers/
5 │ └── mainController.js
6 ├── services/
7 │ └── accountService.js
8 ├── routes/
9 │ └── user.js✅ Exemplo em conformidade
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
Regras: Qualidade e manutenibilidade do código
5. Simplifique o fluxo de controle com retornos antecipados
Em vez de aninhamento profundo de if/else, retorne cedo quando as condições falharem.
❌ Exemplo não em conformidade
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};✅ Exemplo em conformidade
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. Evite aninhamento excessivo em controllers
Evite grandes blocos de lógica aninhada dentro de controllers ou services. Extraia condições repetidas ou complexas para funções auxiliares ou utilitários bem nomeados.
❌ Exemplo não em conformidade
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};✅ Exemplo em conformidade
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. Mantenha a lógica de negócios fora dos controllers
Controladores devem permanecer enxutos e apenas orquestrar requisições. Mova a lógica de negócio para os serviços.
❌ Exemplo não em conformidade
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};✅ Exemplo em conformidade
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. Use funções de utilidade para padrões repetidos
Padrões duplicados (por exemplo, validação, formatação) devem residir em utilitários compartilhados.
❌ Exemplo não em conformidade
// 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 } });
},
};✅ Exemplo em conformidade
// 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. Remova os logs de depuração antes da produção
Não use console.log, console.warn ou console.error em código de produção. Sempre use strapi.log ou um logger configurado para garantir que os logs respeitem as configurações do ambiente e evitem expor informações sensíveis.
❌ Exemplo não em conformidade
// 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;
},
};✅ Exemplo em conformidade
// 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);
}
Regras: Práticas de banco de dados e consultas
10. Evite consultas SQL brutas
Não execute consultas SQL brutas em controllers ou serviços. Sempre use um método de consulta consistente e de alto nível (como um ORM ou query builder) para garantir a manutenibilidade, aplicar regras/hooks e reduzir riscos de segurança.
❌ Exemplo não em conformidade
// 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;
},
};✅ Exemplo em conformidade
// src/api/user/services/user.js
module.exports = {
async findActiveUsers() {
return await strapi.db.query('api::user.user').findMany({
where: { active: true },
});
},
};11. Use o query engine do Strapi consistentemente
Não misture diferentes métodos de acesso a banco de dados (por exemplo, chamadas ORM vs. consultas brutas) dentro da mesma funcionalidade. Use uma abordagem de consulta única e consistente para garantir manutenibilidade, legibilidade e comportamento previsível.
❌ Exemplo não em conformidade
// 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 };
},
};✅ Exemplo em conformidade
// src/api/order/services/order.js
module.exports = {
async getPendingOrders() {
return await strapi.db.query('api::order.order').findMany({
where: { status: 'pending' },
});
},
};12. Otimize as chamadas ao banco de dados
Agrupe consultas de banco de dados relacionadas ou combine-as em uma única operação para evitar gargalos de desempenho e reduzir chamadas sequenciais desnecessárias.
❌ Exemplo não em conformidade
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;
}✅ Exemplo em conformidade
async function getArticlesWithAuthors() {
return await db.query('articles').findMany({ populate: ['author'] });
}
Regras: API e Segurança
13. Valide a entrada com os validadores do Strapi
Nunca confie na entrada de clientes ou fontes externas. Valide todos os dados de entrada usando um mecanismo de validação consistente antes de usá-los em controladores, serviços ou operações de banco de dados.
❌ Exemplo não em conformidade
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);
}✅ Exemplo em conformidade
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. Sanitize a entrada do usuário antes de salvar
Sanitize todas as entradas antes de salvá-las no banco de dados ou passá-las para outros sistemas.
❌ Exemplo não em conformidade
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);
}✅ Exemplo em conformidade
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. Aplique verificações de permissão
Aplique verificações de permissão em cada rota protegida para garantir que apenas usuários autorizados possam acessá-la.
❌ Exemplo não em conformidade
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 });
}✅ Exemplo em conformidade
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. Tratamento consistente de erros com Boom
Trate erros de forma consistente em todas as rotas de API usando um mecanismo de tratamento de erros centralizado ou unificado.
❌ Exemplo não em conformidade
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
}
}✅ Exemplo em conformidade
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 };
Regras: Testes e documentação
17. Adicione ou atualize testes para cada funcionalidade
Novo código sem testes não será mesclado, testes fazem parte da definição de pronto (definition of done).
❌ Exemplo não em conformidade
// 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✅ Exemplo em conformidade
// 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. Documente novos endpoints
Toda adição de API deve ser documentada nos docs de referência antes do merge.
❌ Exemplo não em conformidade
// 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✅ Exemplo em conformidade
// 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 };
},
};Exemplo de Atualização de Documentos de Referência:
### POST /users/deactivate
**Request Body:**
```json
{
"userId": "string"
}Resposta:
{
"success": true
}Erros:
- 400: userId é obrigatório
- 404: Usuário não encontrado
Por que isso funciona:
- Desenvolvedores e consumidores de API podem descobrir e usar endpoints de forma confiável
- Garante consistência entre implementação e documentação
- Facilita a manutenção e o onboarding
---
Você quer que eu continue com a **Regra #19 (“Use JSDoc para Utilitários Compartilhados”)** no mesmo formato a seguir?19. Use JSDoc para utilitários compartilhados
Funções compartilhadas devem ser explicadas com JSDoc para facilitar o onboarding e a colaboração.
❌ Exemplo não em conformidade
// src/utils/slugify.js
function slugify(text) {
return text.toLowerCase().trim().replace(/\s+/g, '-');
}
module.exports = slugify;✅ Exemplo em conformidade
// 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. Atualizar changelog a cada PR significativo
Atualize o changelog do projeto com cada recurso significativo, correção de bug ou alteração de API antes de fazer merge de um PR.
❌ Exemplo não em conformidade
# CHANGELOG.md
## [1.0.0] - 2025-09-01
- Lançamento inicial✅ Exemplo em conformidade
# CHANGELOG.md
## [1.1.0] - 2025-10-06
- Adicionado endpoint de desativação de usuário (`POST /users/deactivate`)
- Corrigido bug na geração de slug para títulos de artigos
- Serviço de notificação por e-mail atualizado para lidar com envio em loteConclusão
Estudamos o repositório público do Strapi para entender como padrões de código consistentes ajudam grandes projetos de código aberto a crescer sem perder qualidade. Estas 20 regras não são teoria. São lições práticas tiradas diretamente da base de código do Strapi que tornam o projeto mais fácil de manter, mais seguro e mais fácil de ler.
Se seu projeto está crescendo, adote estas lições e aplique-as em suas revisões de código. Elas ajudarão você a gastar menos tempo limpando código bagunçado e mais tempo construindo funcionalidades que realmente importam.
.avif)
