
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. Se necesitan reglas de revisión del código claras y coherentes para garantizar que todas las contribuciones sean fiables, legibles y seguras.
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 reales, discusiones y pull requests que ayudaron al proyecto a crecer mientras se mantenía la estabilidad del código base.
Por qué es difícil mantener la calidad del código en un gran proyecto de código abierto
Mantener la calidad en un gran proyecto de código abierto es un reto debido a la magnitud y diversidad de las contribuciones. Cientos o incluso miles de desarrolladores, desde voluntarios a ingenieros experimentados, envían pull requests, cada uno introduciendo nuevas características, correcciones de errores o refactorizaciones. Sin reglas claras, el código base puede volverse incoherente, frágil o difícil de navegar.
Algunos de los principales retos son:
- Colaboradores diversos con distintos niveles de experiencia.
- Patrones de codificación incoherentes entre módulos.
- Bugs ocultos y lógica duplicada arrastrándose.
- Riesgos de seguridad si no se aplican los procesos.
- Revisiones que requieren mucho tiempo para los voluntarios que no están familiarizados con todo el código base.
Para afrontar estos retos, los proyectos de éxito se basan en procesos estructurados: normas compartidas, herramientas automatizadas y directrices claras. Estas prácticas garantizan el mantenimiento, la legibilidad y la seguridad incluso cuando el proyecto crece y atrae a más colaboradores.
Cómo el cumplimiento de estas normas mejora la capacidad de mantenimiento, la seguridad y la integración.
La adhesión a un conjunto claro de normas de revisión del código repercute directamente en la salud del proyecto:
- Facilidad de mantenimiento: Las estructuras de carpetas, las convenciones de nomenclatura y los patrones de codificación coherentes facilitan la lectura, la navegación y la ampliación del código base.
- Seguridad: La validación de entradas, el saneamiento, las comprobaciones de permisos y el acceso controlado a las bases de datos reducen las vulnerabilidades y evitan las fugas accidentales de datos.
- Incorporación más rápida: Las normas compartidas, las utilidades documentadas y los ejemplos claros ayudan a los nuevos colaboradores a entender el proyecto rápidamente y a contribuir con confianza.
Aplicando estas reglas, los equipos pueden garantizar que la base de código siga siendo escalable, fiable y segura, incluso aunque aumente el número de colaboradores.
Entre el contexto y las normas
Antes de ver las reglas, es importante entender que mantener la calidad del código alta en un proyecto como Strapi no se trata sólo de seguir las mejores prácticas generales. Se trata de tener patrones y estándares claros que ayuden a cientos de colaboradores a estar en la misma página. Cada una de las 20 reglas siguientes se centra en retos reales que aparecen en el código base de Strapi.
Los ejemplos que se ofrecen para cada norma ilustran tanto los planteamientos no conformes como los conformes, lo que da una idea clara de cómo se aplican estos principios en la práctica.
Ahora, exploremos las reglas que hacen que el código base de Strapi sea escalable, consistente y de alta calidad, empezando por la estructura del proyecto y los estándares de configuración.
Normas: Estructura y coherencia del proyecto
1. Siga las convenciones de carpetas establecidas por Strapi
Evite dispersar archivos o inventar nuevas estructuras. Cíñete al diseño de proyecto establecido por Strapi para que la navegación sea predecible.
❌ Ejemplo de no conformidad
1src/
2├──controllers/
3│└── userController.js
4├──servicios/
5│└── userLogic.js
6├──rutas/
7│└── userRoutes.js
8└──utils/
9 └── helper.js✅ Ejemplo de conformidad
1src/
2└──api/
3 └── usuario/
4 ├── controladores/
5 │ └── user.js
6 ├── servicios/
7 │ └── user.js
8 ├── rutas/
9 │ └── user.js
10 └── content-types/
11 └── user/schema.json2. Mantener la coherencia de los archivos de configuración
Utilice la misma estructura, nomenclatura y convenciones de formato en todos los archivos de configuración para garantizar la coherencia y evitar errores.
❌ Ejemplo de no conformidad
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 de conformidad
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 estricta seguridad de tipo
Todo el código nuevo o actualizado debe incluir tipos TypeScript precisos o definiciones JSDoc. Evite el uso de cualquier tipo de retorno o la inferencia implícita de tipos en módulos compartidos.
❌ Ejemplo de no conformidad
1// src/api/user/services/user.ts
2export const createUser = (data) => {
3 return strapi.db.query('api::user.user').create({ data });
4};✅ Ejemplo de conformidad
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. Nombres coherentes para servicios y controladores
Los nombres de controladores y servicios deben coincidir claramente con su dominio (por ejemplo, usuario.controlador.js con usuario.servicio.js).
❌ Ejemplo de no conformidad
1src/
2└── api/
3 └── usuario/
4 ├── controladores/
5 │ └── mainController.js
6 ├── servicios/
7 │ └── accountService.js
8 ├── rutas/
9 │ └── usuario.js✅ Ejemplo de conformidad
1src/
2└── api/
3 └── usuario/
4 ├── controladores/
5 │ └── usuario.js
6 ├── servicios/
7 │ └── usuario.js
8 ├── rutas/
9 │ └── usuario.js
10 └── contenido-tipos/
11 └── usuario/esquema.json
Normas: Calidad y mantenimiento del código
5. Simplificar el flujo de control con devoluciones anticipadas
En lugar de un profundo anidamiento if/else, devuelva pronto cuando las condiciones fallen.
❌ Ejemplo de no conformidad
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 de conformidad
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 los controladores
Evite grandes bloques de lógica anidada dentro de controladores o servicios. Extraiga las condiciones repetidas o complejas en funciones de ayuda o utilidades bien denominadas.
❌ Ejemplo de no conformidad
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 de conformidad
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 permanecer delgados y sólo orquestar las solicitudes. Traslade la lógica empresarial a los servicios.
❌ Ejemplo de no conformidad
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 de conformidad
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 (por ejemplo, validación, formato) deben vivir en utilidades compartidas.
❌ Ejemplo de no conformidad
// 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 de conformidad
// src/utils/slugify.js
módulo.exports = (texto) => 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 logger configurado para asegurar que los logs respetan la configuración del entorno y evitan exponer información sensible.
❌ Ejemplo de no conformidad
// 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 de conformidad
// 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);
}
Normas: Base de datos y prácticas de consulta
10. Evite las consultas SQL sin procesar
No ejecute consultas SQL sin procesar en controladores o servicios. Utilice siempre un método de consulta coherente y de alto nivel (como un ORM o un generador de consultas) para garantizar la capacidad de mantenimiento, aplicar reglas/ganchos y reducir los riesgos de seguridad.
❌ Ejemplo de no conformidad
// 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 de conformidad
// 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 consulta de Strapi de forma coherente
No mezcle diferentes métodos de acceso a la base de datos (por ejemplo, llamadas ORM frente a consultas sin procesar) dentro de la misma función.Utilice un enfoque de consulta único y coherente para garantizar la mantenibilidad, la legibilidad y el comportamiento predecible.
❌ Ejemplo de no conformidad
// 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 de conformidad
// 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
Realice consultas a bases de datos relacionadas por lotes o combínelas en una sola operación para evitar cuellos de botella en el rendimiento y reducir las llamadas secuenciales innecesarias.
❌ Ejemplo de no conformidad
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 de conformidad
async function getArticlesWithAuthors() {
return await db.query('articles').findMany({ populate: ['author'] });
}
Normas: API y seguridad
13. Validar la entrada con validadores Strapi
No confíe nunca en la información procedente de clientes o fuentes externas. Valide todos los datos entrantes mediante un mecanismo de validación coherente antes de utilizarlos en controladores, servicios u operaciones de bases de datos.
❌ Ejemplo de no conformidad
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 de conformidad
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
Desinfecte todas las entradas antes de guardarlas en la base de datos o pasarlas a otros sistemas.
❌ Ejemplo de no conformidad
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 de conformidad
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 controles de permisos
Aplique comprobaciones de permisos en cada ruta protegida para asegurarse de que sólo los usuarios autorizados pueden acceder a ella.
❌ Ejemplo de no conformidad
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 de conformidad
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. Tratamiento coherente de errores con Boom
Gestione los errores de forma coherente en todas las rutas API mediante un mecanismo de gestión de errores centralizado o unificado.
❌ Ejemplo de no conformidad
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 de conformidad
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 };
Normas: Pruebas y documentación
17. Añadir o actualizar pruebas para cada característica
El código nuevo sin pruebas no se fusionará, las pruebas forman parte de la definición de hecho.
❌ Ejemplo de no conformidad
// 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 de conformidad
// 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 puntos finales
Cada adición a la API debe documentarse en los documentos de referencia antes de la fusión.
❌ Ejemplo de no conformidad
// 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 de conformidad
// 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 documentos 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 usuarios de API pueden descubrir y utilizar los puntos finales 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?19. Utilizar JSDoc para utilidades compartidas
Las funciones compartidas deben explicarse con JSDoc para facilitar la incorporación y la colaboración.
❌ Ejemplo de no conformidad
// src/utils/slugify.js
function slugify(text) {
return text.toLowerCase().trim().replace(/\s+/g, '-');
}
module.exports = slugify;✅ Ejemplo de conformidad
// 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 registro de cambios con cada PR significativo
Actualice el registro de cambios del proyecto con cada característica significativa, corrección de errores o cambio de API antes de fusionar un PR.
❌ Ejemplo de no conformidad
# CHANGELOG.md
## [1.0.0] - 2025-09-01
- Versión inicial✅ Ejemplo de conformidad
# CHANGELOG.md
## [1.1.0] - 2025-10-06
- Añadido punto final de desactivación de usuarios (`POST /users/deactivate`)
- Corregido error en la generación de slug 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 entender cómo unos patrones de código coherentes ayudan a los grandes proyectos de código abierto a crecer sin perder calidad. Estas 20 reglas no son teoría. Son lecciones prácticas tomadas directamente del código base de Strapi que hacen que el proyecto sea más fácil de mantener, más seguro y más fácil de leer.
Si tu proyecto está creciendo, aprovecha estas lecciones y aplícalas a tus revisiones de código. Te ayudarán a pasar menos tiempo limpiando código desordenado y a dedicar más tiempo a crear funciones que realmente importan.
.avif)
