
Introducción
Strapi es una de las plataformas CMS headless de código abierto más populares, pero también es una enorme base de código con cientos de colaboradores y miles de pull requests. Mantener la calidad de un proyecto tan grande no es fácil. Requiere reglas de revisión de código claras y consistentes para asegurar que cada contribución sea fiable, legible y segura.
En este artículo, hemos reunido un conjunto de reglas de revisión de código basadas en el repositorio público de Strapi. Estas reglas provienen de trabajo real: problemas, discusiones y solicitudes de extracción (pull requests) que ayudaron al proyecto a crecer manteniendo la base de código estable.
Por qué es difícil mantener la calidad del código en un gran proyecto de código abierto
Mantener la calidad en un proyecto de código abierto grande es un desafío debido a la magnitud y diversidad de las contribuciones. Cientos o incluso miles de desarrolladores, desde voluntarios hasta ingenieros experimentados, envían pull requests, cada uno introduciendo nuevas funcionalidades, correcciones de errores o refactorizaciones. Sin reglas claras, la base de código puede volverse rápidamente inconsistente, frágil o difícil de navegar.
Algunos de los principales desafíos incluyen:
- Colaboradores diversos con diferentes niveles de experiencia.
- Patrones de codificación inconsistentes entre módulos.
- Errores ocultos y lógica duplicada que se van introduciendo.
- Riesgos de seguridad si no se aplican los procesos.
- Revisiones que consumen mucho tiempo para voluntarios no familiarizados con la totalidad del código base.
Para abordar estos desafíos, los proyectos exitosos se basan en procesos estructurados: estándares compartidos, herramientas automatizadas y directrices claras. Estas prácticas garantizan la mantenibilidad, la legibilidad y la seguridad, incluso a medida que el proyecto crece y atrae a más colaboradores.
Cómo seguir estas reglas mejora la mantenibilidad, la seguridad y la incorporación
Adherirse a un conjunto claro de reglas de revisión de código tiene un impacto directo en la salud de tu proyecto:
- Mantenibilidad: Las estructuras de carpetas, las convenciones de nomenclatura y los patrones de codificación consistentes facilitan la lectura, navegación y extensión de la base de código.
- Seguridad: La validación de entradas, la sanitización, las comprobaciones de permisos y el acceso controlado a la base de datos reducen las vulnerabilidades y previenen fugas de datos accidentales.
- Onboarding más rápido: Estándares compartidos, utilidades documentadas y ejemplos claros ayudan a los nuevos colaboradores a comprender el proyecto rápidamente y a contribuir con confianza.
Al aplicar estas reglas, los equipos pueden asegurar que la base de código permanezca escalable, fiable y segura, incluso a medida que aumenta el número de colaboradores.
Conectando el contexto con las reglas
Antes de analizar las reglas, es importante comprender que mantener una alta calidad del código en un proyecto como Strapi no solo consiste en seguir las mejores prácticas generales. Se trata de contar con patrones y estándares claros que ayuden a cientos de colaboradores a mantenerse en sintonía. Cada una de las 20 reglas que se enumeran a continuación se centra en retos reales que aparecen en el código base de Strapi.
Los ejemplos proporcionados para cada regla ilustran enfoques tanto no conformes como conformes, ofreciendo una imagen clara de cómo se aplican estos principios en la práctica.
Ahora, exploremos las reglas que hacen que la base de código de Strapi sea escalable, consistente y de alta calidad, comenzando por la estructura del proyecto y los estándares de configuración.
Reglas: Estructura y coherencia del proyecto
1. Siga las convenciones de carpetas establecidas de Strapi
Evitar dispersar archivos o inventar nuevas estructuras. Adherirse al diseño de proyecto establecido de Strapi para mantener la navegación predecible.
❌ Ejemplo no conforme
1src/
2├── controllers/
3│ └── userController.js
4├── services/
5│ └── userLogic.js
6├── routes/
7│ └── userRoutes.js
8└── utils/
9 └── helper.js✅ Ejemplo 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. Mantener los archivos de configuración consistentes
Utilice las mismas convenciones de estructura, nomenclatura y formato en todos los archivos de configuración para garantizar la coherencia y evitar errores.
❌ Ejemplo no 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});✅ Ejemplo 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. Mantener una seguridad de tipos estricta
Todo código nuevo o actualizado debe incluir tipos TypeScript precisos o definiciones JSDoc. Evite el uso de 'any', la falta de tipos de retorno o la inferencia de tipos implícita en módulos compartidos.
❌ Ejemplo no conforme
1// src/api/user/services/user.ts
2export const createUser = (data) => {
3 return strapi.db.query('api::user.user').create({ data });
4};✅ Ejemplo 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 consistente para servicios y controladores
Los nombres de los controladores y servicios deben coincidir claramente con su dominio (p. ej., user.controller.js con user.service.js).
❌ Ejemplo no conforme
1src/
2└── api/
3 └── user/
4 ├── controllers/
5 │ └── mainController.js
6 ├── services/
7 │ └── accountService.js
8 ├── routes/
9 │ └── user.js✅ Ejemplo 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
Reglas: Calidad y mantenibilidad del código
5. Simplificar el flujo de control con retornos anticipados
En lugar de anidamientos profundos de if/else, retorna anticipadamente cuando las condiciones fallen.
❌ Ejemplo no 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};✅ Ejemplo 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. Evitar el anidamiento excesivo en controladores
Evitar grandes bloques de lógica anidada dentro de controladores o servicios. Extraer condiciones repetidas o complejas en funciones auxiliares o utilidades con nombres claros.
❌ Ejemplo no 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};✅ Ejemplo 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. Mantener la lógica de negocio fuera de los controladores
Los controladores deben ser ligeros y solo orquestar las solicitudes. Mueva la lógica de negocio a los servicios.
❌ Ejemplo no 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};✅ Ejemplo 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. Utilizar funciones de utilidad para patrones repetidos
Los patrones duplicados (p. ej., validación, formato) deberían residir en utilidades compartidas.
❌ Ejemplo no 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 } });
},
};✅ Ejemplo 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. Eliminar los registros de depuración antes de la producción
No utilices console.log, console.warn o console.error en código de producción. Utiliza siempre strapi.log o un registrador configurado para asegurar que los registros respeten la configuración del entorno y evitar exponer información sensible.
❌ Ejemplo no 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;
},
};✅ Ejemplo 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);
}
Reglas: Bases de datos y prácticas de consulta
10. Evite las consultas SQL directas
No ejecutes consultas SQL directas en controladores o servicios. Utiliza siempre un método de consulta consistente y de alto nivel (como un ORM o un constructor de consultas) para garantizar la mantenibilidad, aplicar reglas/hooks y reducir los riesgos de seguridad.
❌ Ejemplo no 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;
},
};✅ Ejemplo conforme
// src/api/user/services/user.js
module.exports = {
async findActiveUsers() {
return await strapi.db.query('api::user.user').findMany({
where: { active: true },
});
},
};11. Utilizar el motor de consultas de Strapi de forma consistente.
No mezcles diferentes métodos de acceso a la base de datos (por ejemplo, llamadas ORM frente a consultas directas) dentro de la misma funcionalidad. Utiliza un enfoque de consulta único y consistente para garantizar la mantenibilidad, la legibilidad y un comportamiento predecible.
❌ Ejemplo no 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 };
},
};✅ Ejemplo conforme
// src/api/order/services/order.js
module.exports = {
async getPendingOrders() {
return await strapi.db.query('api::order.order').findMany({
where: { status: 'pending' },
});
},
};12. Optimizar las llamadas a la base de datos
Agrupar las consultas de base de datos relacionadas o combinarlas en una única operación para evitar cuellos de botella de rendimiento y reducir llamadas secuenciales innecesarias.
❌ Ejemplo no 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;
}✅ Ejemplo conforme
async function getArticlesWithAuthors() {
return await db.query('articles').findMany({ populate: ['author'] });
}
Reglas: API y seguridad
13. Validar la entrada con los validadores de Strapi
Nunca confíe en la entrada de clientes o fuentes externas. Valide todos los datos entrantes utilizando un mecanismo de validación consistente antes de usarlos en controladores, servicios u operaciones de base de datos.
❌ Ejemplo no 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);
}✅ Ejemplo 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. Sanear la entrada del usuario antes de guardar
Sanea todas las entradas antes de guardarlas en la base de datos o de pasarlas a otros sistemas.
❌ Ejemplo no 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);
}✅ Ejemplo 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. Aplicar comprobaciones de permisos
Aplicar comprobaciones de permisos en cada ruta protegida para asegurar que solo los usuarios autorizados puedan acceder a ella.
❌ Ejemplo no 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 });
}✅ Ejemplo 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. Manejo consistente de errores con Boom
Maneje los errores de forma consistente en todas las rutas de la API utilizando un mecanismo de gestión de errores centralizado o unificado.
❌ Ejemplo no 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
}
}✅ Ejemplo 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 };
Reglas: Pruebas y documentación
17. Añadir o actualizar pruebas para cada funcionalidad
El código nuevo sin pruebas no se fusionará; las pruebas son parte de la definición de 'terminado'.
❌ Ejemplo no 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✅ Ejemplo 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. Documentar nuevos endpoints
Cada adición de API debe documentarse en la documentación de referencia antes de la fusión.
❌ Ejemplo no 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✅ Ejemplo 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 };
},
};Ejemplo de actualización de la documentación de referencia:
### POST /users/deactivate
**Request Body:**
```json
{
"userId": "string"
}Respuesta:
{
"success": true
}Errores:
- 400: userId es obligatorio
- 404: Usuario no encontrado
Por qué funciona:
- Los desarrolladores y consumidores de API pueden descubrir y usar los endpoints de forma fiable
- Garantiza la coherencia entre la implementación y la documentación
- Facilita el mantenimiento y la incorporación
---
¿Quieres que continúe con la **Regla #19 (“Usar JSDoc para utilidades compartidas”)** en el mismo formato a continuación?19. Usar JSDoc para utilidades compartidas
Las funciones compartidas deben explicarse con JSDoc para facilitar la incorporación y la colaboración.
❌ Ejemplo no conforme
// src/utils/slugify.js
function slugify(text) {
return text.toLowerCase().trim().replace(/\s+/g, '-');
}
module.exports = slugify;✅ Ejemplo 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. Actualizar el changelog con cada PR significativo
Actualiza el registro de cambios del proyecto con cada característica significativa, corrección de errores o cambio en la API antes de fusionar una PR.
❌ Ejemplo no conforme
# CHANGELOG.md
## [1.0.0] - 2025-09-01
- Versión inicial✅ Ejemplo conforme
# CHANGELOG.md
## [1.1.0] - 2025-10-06
- Añadido endpoint de desactivación de usuario (`POST /users/deactivate`)
- Corregido error en la generación de slugs para títulos de artículos
- Actualizado el servicio de notificación por correo electrónico para gestionar el envío por lotesConclusión
Estudiamos el repositorio público de Strapi para comprender cómo los patrones de código consistentes ayudan a los grandes proyectos de código abierto (open-source) a crecer sin perder calidad. Estas 20 reglas no son teoría. Son lecciones prácticas extraídas directamente del codebase de Strapi que hacen que el proyecto sea más fácil de mantener, más seguro y más legible.
Si tu proyecto está creciendo, toma estas lecciones y aplícalas a tus revisiones de código. Te ayudarán a pasar menos tiempo limpiando código desordenado y más tiempo desarrollando funcionalidades que realmente importan.
.avif)
