
Introdução
O Strapi é uma das mais populares plataformas CMS sem cabeça de código aberto, mas é também uma enorme base de código com centenas de colaboradores e milhares de pedidos de atualização. Manter a qualidade de um projeto tão grande não é fácil. São necessárias regras de revisão de código claras e consistentes para garantir que cada contribuição se mantém fiável, legível e segura.
Neste artigo, reunimos 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 reais, discussões e pull requests que ajudaram o projeto a crescer, mantendo a base de código estável.
Porque é que é difícil manter a qualidade do código num grande projeto de código aberto
Manter a qualidade num grande projeto de código aberto é um desafio devido à enorme escala e diversidade de contribuições. Centenas ou mesmo milhares de programadores, desde voluntários a engenheiros experientes, submetem pedidos pull, cada um introduzindo novas funcionalidades, correcções de erros ou refacções. Sem regras claras, a base de código pode rapidamente tornar-se inconsistente, frágil ou difícil de navegar.
Alguns dos principais desafios incluem:
- Colaboradores diversificados com diferentes níveis de experiência.
- Padrões de codificação inconsistentes entre módulos.
- Bugs ocultos e lógica duplicada a aparecer.
- Riscos de segurança se os processos não forem aplicados.
- Revisões demoradas para voluntários que não estão familiarizados com a base de código completa.
Para enfrentar estes desafios, os projectos bem sucedidos baseiam-se em processos estruturados: normas partilhadas, ferramentas automatizadas e diretrizes claras. Estas práticas garantem a facilidade de manutenção, a legibilidade e a segurança, mesmo quando o projeto cresce e atrai mais colaboradores.
Como o cumprimento destas regras melhora a manutenção, a segurança e a integração
A adesão a um conjunto claro de regras de revisão de código tem um impacto direto na saúde do seu projeto:
- Facilidade de manutenção: Estruturas de pastas, convenções de nomenclatura e padrões de codificação consistentes facilitam a leitura, a navegação e a extensão da base de código.
- Segurança: A validação de entradas, a higienização, as verificações de permissões e o acesso controlado à base de dados reduzem as vulnerabilidades e evitam fugas acidentais de dados.
- Integração mais rápida: Normas partilhadas, utilitários documentados e exemplos claros ajudam os novos colaboradores a compreender rapidamente o projeto e a contribuir com confiança.
Ao aplicar estas regras, as equipas podem garantir que a base de código se mantém escalável, fiável e segura, mesmo quando o número de colaboradores aumenta.
Ligar o contexto às regras
Antes de analisar as regras, é importante compreender que manter a qualidade do código elevada num projeto como o Strapi não se trata apenas de seguir as melhores práticas gerais. Trata-se de ter padrões e normas claras que ajudam centenas de colaboradores a manterem-se na mesma página. Cada uma das 20 regras abaixo foca em desafios reais que aparecem na base de código do Strapi.
Os exemplos fornecidos para cada regra ilustram tanto abordagens não conformes como conformes, dando uma imagem clara de como estes 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 com a estrutura do projeto e as normas de configuração.
Regras: Estrutura e coerência do projeto
1. Seguir as convenções de pastas estabelecidas pelo Strapi
Evite dispersar ficheiros ou inventar novas estruturas. Siga o esquema de projeto estabelecido pelo Strapi para manter a navegação previsível.
Exemplo de não conformidade
1src/
2├──controllers/
3│└─── userController.js
4├───services/
5│└── userLogic.js
6├───routes/
7│└─── userRoutes.js
8└──utils/
9 └─── helper.jsExemplo de conformidade
1src/
2└──api/
3 └─── user/
4 ├─── controladores/
5 │ └─── user.js
6 ├─── services/
7 │ └─── user.js
8 ├─── routes/
9 │ └─── user.js
10 └── content-types/
11 └── user/schema.json2. Manter os ficheiros de configuração consistentes
Utilize a mesma estrutura, nomenclatura e convenções de formatação em todos os ficheiros de configuração para garantir a consistência e evitar erros.
Exemplo de não 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 de 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 uma segurança de tipo rigorosa
Todo o código novo ou atualizado deve incluir tipos TypeScript precisos ou definições JSDoc. Evite utilizar quaisquer tipos de retorno em falta ou inferência de tipo implícita em módulos partilhados.
Exemplo de não conformidade
1// src/api/user/services/user.ts
2export const createUser = (data) => {
3 return strapi.db.query('api::user.user').create({ data });
4};Exemplo de 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 do controlador e do serviço devem corresponder claramente ao seu domínio (por exemplo, user.controller.js com user.service.js).
Exemplo de não conformidade
1src/
2└── api/
3 └── utilizador/
4 ├── controladores/
5 │ └── mainController.js
6 ├── serviços/
7 │ └── accountService.js
8 ├── rotas/
9 │ └── utilizador.jsExemplo de conformidade
1src/
2└── api/
3 └── utilizador/
4 ├── controladores/
5 │ └── utilizador.js
6 ├── serviços/
7 │ └── utilizador.js
8 ├── rotas/
9 │ └── utilizador.js
10 └── conteúdo-tipos/
11 └── utilizador/esquema.json
Regras: Qualidade do código e facilidade de manutenção
5. Simplificar o fluxo de controlo com devoluções antecipadas
Em vez de um aninhamento if/else profundo, devolver cedo quando as condições falham.
Exemplo de não 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 de 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. Evitar a colocação excessiva de ninhos nos controladores
Evite grandes blocos de lógica aninhada dentro de controladores ou serviços. Extraia condições repetidas ou complexas para funções auxiliares ou utilitários bem nomeados.
Exemplo de não 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 de 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. Manter a lógica comercial fora dos controladores
Os controladores devem permanecer finos e apenas orquestrar pedidos. Mova a lógica empresarial para os serviços.
Exemplo de não 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 de 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. Utilizar funções de utilidade para padrões repetidos
Os padrões duplicados (por exemplo, validação, formatação) devem residir em utilitários partilhados.
Exemplo de não 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 de conformidade
// src/utils/slugify.js
módulo.exportações = (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. Remover os registos de depuração antes da produção
Não utilize console.log, console.warn ou console.error no código de produção. Utilize sempre strapi.log ou um registador configurado para garantir que os registos respeitam as definições de ambiente e evitam a exposição de informações sensíveis.
Exemplo de não 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 de 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: Base de dados e práticas de consulta
10. Evitar consultas SQL em bruto
Não execute consultas SQL brutas em controladores ou serviços. Utilize sempre um método de consulta consistente e de alto nível (como um ORM ou um construtor de consultas) para garantir a manutenção, aplicar regras/hooks e reduzir os riscos de segurança.
Exemplo de não 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 de conformidade
// src/api/user/services/user.js
module.exports = {
async findActiveUsers() {
return await strapi.db.query('api::user.user').findMany({
where: { active: true },
});
},
};11. Utilizar o motor de pesquisa do Strapi de forma consistente
Não misture diferentes métodos de acesso à base de dados (por exemplo, chamadas ORM vs. consultas brutas) na mesma funcionalidade. Utilize uma abordagem de consulta única e consistente para garantir a manutenção, a legibilidade e o comportamento previsível.
Exemplo de não 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 de conformidade
// src/api/order/services/order.js
module.exports = {
async getPendingOrders() {
return await strapi.db.query('api::order.order').findMany({
where: { status: 'pending' },
});
},
};12. Otimizar as chamadas à base de dados
Consultas de bases de dados relacionadas em lote ou combinadas numa única operação para evitar estrangulamentos de desempenho e reduzir chamadas sequenciais desnecessárias.
Exemplo de não 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 de conformidade
async function getArticlesWithAuthors() {
return await db.query('articles').findMany({ populate: ['author'] });
}
Regras: API e segurança
13. Validar a entrada com os validadores Strapi
Validar todos os dados recebidos utilizando um mecanismo de validação consistente antes de os utilizar em controladores, serviços ou operações de bases de dados.
Exemplo de não 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 de 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. Limpar a entrada do utilizador antes de guardar
Limpe todas as entradas antes de as guardar na base de dados ou de as transmitir a outros sistemas.
Exemplo de não 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 de 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. Aplicar controlos de permissões
Aplique verificações de permissão em todas as rotas protegidas para garantir que apenas os utilizadores autorizados podem aceder às mesmas.
Exemplo de não 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 de 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 coerente de erros com o Boom
Tratar os erros de forma consistente em todas as rotas da API utilizando um mecanismo de tratamento de erros centralizado ou unificado.
Exemplo de não 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 de 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. Adicionar ou atualizar testes para cada funcionalidade
O novo código sem testes não será integrado, os testes fazem parte da definição de "feito".
Exemplo de não 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 serviceExemplo de 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. Documentar novos parâmetros
Cada adição de API deve ser documentada na documentação de referência antes da fusão.
Exemplo de não 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 docsExemplo de 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 dos documentos de referência:
### POST /users/deactivate
**Request Body:**
```json
{
"userId": "string"
}Resposta:
{
"success": true
}Erros:
- 400: userId é obrigatório
- 404: Utilizador não encontrado
Porque é que isto funciona:
- Os programadores e os consumidores de API podem descobrir e utilizar pontos finais de forma fiável
- Garante a consistência entre a implementação e os documentos
- Facilita a manutenção e a integração
---
Quer que continue com a **Regra#19 ("Utilizar JSDoc para utilitários partilhados")** no mesmo formato a seguir?19. Utilizar o JSDoc para utilitários partilhados
As funções partilhadas devem ser explicadas com o JSDoc para facilitar a integração e a colaboração.
Exemplo de não conformidade
// src/utils/slugify.js
function slugify(text) {
return text.toLowerCase().trim().replace(/\s+/g, '-');
}
module.exports = slugify;Exemplo de 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 o changelog com cada PR significativo
Actualize o registo de alterações do projeto com todas as funcionalidades significativas, correcções de erros ou alterações da API antes de fundir um PR.
Exemplo de não conformidade
# CHANGELOG.md
## [1.0.0] - 2025-09-01
- Lançamento inicialExemplo de conformidade
# CHANGELOG.md
## [1.1.0] - 2025-10-06
- Adicionado endpoint de desativação de utilizadores (`POST /users/deactivate`)
- Corrigido o erro na geração de slug para títulos de artigos
- Serviço de notificação por e-mail atualizado para lidar com o envio em loteConclusão
Estudámos o repositório público do Strapi para compreender como padrões de código consistentes ajudam grandes projectos de código aberto a crescer sem perder qualidade. Estas 20 regras não são teoria. São lições práticas retiradas 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 o seu projeto está a crescer, aproveite estas lições e aplique-as às suas revisões de código. Ajudá-lo-ão a passar menos tempo a limpar código confuso e mais tempo a criar funcionalidades que realmente interessam.
.avif)
