Strapi Patterns
Guía Rápida: Usa Strapi como un CMS headless de código abierto con APIs REST/GraphQL generadas automáticamente a partir de esquemas de tipos de contenido. En v5, usa la API de Servicio de Documentos (
strapi.documents()) para el acceso a datos en el backend en lugar del Entity Service obsoleto. Las respuestas de la API REST usan un formato plano (data.fieldName, nodata.attributes.fieldName). Las relaciones y los medios NO se incluyen por defecto: siempre pasapopulate. Usaqspara construir cadenas de consulta complejas. Los tipos de contenido son privados por defecto; configura los permisos a través del plugin de Usuarios y Permisos o tokens de API.
<critical_requirements>
CRÍTICO: Antes de Usar Esta Habilidad
Todo el código debe seguir las convenciones del proyecto en CLAUDE.md (kebab-case, exports con nombre, orden de importaciones,
import type, constantes con nombre)
(DEBES usar strapi.documents('api::tipo-contenido.tipo-contenido') (API de Servicio de Documentos) para todo el acceso a datos en el backend en Strapi v5 – la API de Entity Service está eliminada)
(DEBES pasar siempre populate cuando necesites relaciones, medios, componentes o zonas dinámicas – Strapi NO devuelve relaciones por defecto)
(DEBES usar la biblioteca qs para construir cadenas de consulta complejas de la API REST con filtros, populate y ordenación – la construcción manual de cadenas se rompe con parámetros anidados)
(DEBES sanitizar y validar tanto la entrada como la salida en controladores personalizados usando this.sanitizeQuery(ctx), this.sanitizeOutput() y this.validateQuery(ctx))
(DEBES configurar permisos para cada endpoint de tipo de contenido a través del panel de administración o la configuración – todas las rutas son privadas por defecto)
</critical_requirements>
Detección automática: Strapi, strapi, @strapi/strapi, createCoreController, createCoreService, createCoreRouter, Document Service, strapi.documents, content-type, schema.json, api::, plugin::, lifecycle hooks, Users & Permissions, /api/auth/local, populate, qs.stringify
Cuándo usarlo:
- Construir aplicaciones gestionadas por contenido con Strapi como CMS headless
- Definir esquemas de tipos de contenido (
schema.json) con campos, relaciones, componentes y zonas dinámicas - Consultar la API REST con filtros, populate, ordenación y paginación
- Crear controladores, servicios, rutas, políticas o middlewares personalizados
- Usar la API de Servicio de Documentos para operaciones CRUD en el backend con flujos de trabajo de borrador/publicación
- Implementar autenticación JWT con el plugin de Usuarios y Permisos
- Agregar hooks de ciclo de vida a los tipos de contenido para efectos secundarios
- Generar tipos TypeScript para los esquemas de contenido
Patrones clave cubiertos:
- Definición de esquemas de tipos de contenido (
schema.json) - Consultas a la API REST con
qs(filtros, populate, ordenación, paginación) - API de Servicio de Documentos (
findMany,findOne,create,update,delete,publish,unpublish) - Controladores, servicios, rutas, políticas y middlewares personalizados
- Hooks de ciclo de vida (
beforeCreate,afterUpdate, etc.) - Autenticación JWT (registro, inicio de sesión, solicitudes autenticadas)
- Generación de tipos TypeScript
Cuándo NO usarlo:
- Plataformas CMS que no sean Strapi (usa la habilidad dedicada para tu CMS)
- Consultas directas a la base de datos que eviten la capa de API de Strapi (usa el Servicio de Documentos de Strapi)
- Lógica transaccional compleja que requiera SQL puro (Strapi abstrae la base de datos)
Recursos detallados:
- Para marcos de decisión y tablas de referencia rápida, consulta reference.md
API Core y REST:
- examples/core.md – Esquemas de tipos de contenido, consultas a la API REST, API de Servicio de Documentos, manejo de errores
Personalización del Backend:
- examples/backend.md – Controladores, servicios, rutas, políticas, middlewares, hooks de ciclo de vida, middleware del Servicio de Documentos
Autenticación:
- examples/auth.md – Autenticación JWT, registro, inicio de sesión, roles y permisos
Filosofía
Strapi es un CMS headless de código abierto construido sobre Node.js (Koa) que genera automáticamente APIs RESTful y GraphQL a partir de esquemas de tipos de contenido. El contenido se define mediante esquemas JSON, se gestiona a través de un panel de administración y se consume a través de endpoints de API generados.
Principios fundamentales:
- Contenido basado en esquemas – Los tipos de contenido se definen en archivos
schema.jsonque describen campos, relaciones, componentes y zonas dinámicas. El Content-Type Builder del panel de administración proporciona un editor visual, pero los esquemas son código que reside en tu repositorio. - APIs generadas automáticamente – Cada tipo de contenido obtiene automáticamente endpoints CRUD REST (
/api/:pluralApiId) y soporte opcional de GraphQL. No se necesita creación manual de rutas/controladores para operaciones estándar. - API de Servicio de Documentos (v5) – La API de backend para acceder al contenido desde código personalizado, plugins y hooks de ciclo de vida. Reemplaza el Entity Service de v4. Usa
documentId(no elidde la base de datos) como identificador principal. - Borrador y Publicación – Los tipos de contenido pueden tener flujos de trabajo de borrador/publicación. El Servicio de Documentos por defecto usa
status: 'draft'; el contenido publicado requierestatus: 'published'o llamadas explícitas apublish(). - Prioridad de permisos – Todos los endpoints de tipos de contenido son privados por defecto. El acceso debe concederse explícitamente a través del panel de administración (plugin de Usuarios y Permisos) o tokens de API.
- Personalización del backend – Los controladores, servicios, rutas, políticas y middlewares se pueden personalizar. Strapi sigue un patrón similar a MVC construido sobre Koa.
Patrones Principales
Patrón 1: Esquema de Tipo de Contenido
Los tipos de contenido se definen en archivos schema.json en ./src/api/[nombre-api]/content-types/[nombre-tipo-contenido]/schema.json. Usa collectionType para contenido de múltiples documentos (artículos, productos) y singleType para contenido de un solo documento (configuración del sitio, página de inicio).
{
"kind": "collectionType",
"info": {
"singularName": "article",
"pluralName": "articles",
"displayName": "Article"
},
"options": { "draftAndPublish": true },
"attributes": {
"title": { "type": "string", "required": true },
"slug": { "type": "uid", "targetField": "title" },
"author": {
"type": "relation",
"relation": "manyToOne",
"target": "api::author.author",
"inversedBy": "articles"
},
"blocks": {
"type": "dynamiczone",
"components": ["blocks.hero", "blocks.rich-text"]
}
}
}
Campos clave: uid genera slugs automáticamente a partir de targetField, los tipos relation definen la cardinalidad con inversedBy/mappedBy, component incrusta bloques reutilizables, dynamiczone permite tipos de componentes mixtos. Consulta examples/core.md para ejemplos completos de colecciones y tipos únicos.
Patrón 2: Consultas a la API REST con qs
La API REST acepta parámetros de consulta complejos para filtrado, población, ordenación y paginación. Usa siempre la biblioteca qs para construir cadenas de consulta.
import qs from "qs";
const query = qs.stringify(
{
filters: { publishedAt: { $notNull: true } },
populate: {
author: { fields: ["name"] },
categories: { fields: ["name", "slug"] },
},
sort: ["publishedAt:desc"],
pagination: { page: 1, pageSize: 25 },
},
{ encodeValuesOnly: true },
);
const response = await fetch(`${STRAPI_URL}/api/articles?${query}`);
const { data, meta } = await response.json();
Puntos clave: encodeValuesOnly evita problemas de codificación de corchetes, los filtros usan operadores ($eq, $notNull, $containsi, $or, $and), populate dirigido con fields evita la sobrecarga (nunca uses populate=* en producción), los componentes de zona dinámica usan la sintaxis on. Consulta examples/core.md para ejemplos completos de filtrado, población y paginación.
Patrón 3: API de Servicio de Documentos (Backend)
La API de Servicio de Documentos se usa en controladores, servicios, hooks de ciclo de vida y plugins personalizados para acceder al contenido desde el lado del servidor. Reemplaza el Entity Service de v4 (eliminado en v5).
const CONTENT_TYPE_UID = "api::article.article";
// Buscar contenido publicado (el valor por defecto es 'draft')
const articles = await strapi.documents(CONTENT_TYPE_UID).findMany({
status: "published",
filters: { categories: { slug: { $eq: "news" } } },
populate: { author: true },
});
// CRUD usa documentId (no el id de la base de datos)
const article = await strapi
.documents(CONTENT_TYPE_UID)
.findOne({ documentId });
const created = await strapi
.documents(CONTENT_TYPE_UID)
.create({ data: { title: "New" } });
await strapi.documents(CONTENT_TYPE_UID).publish({ documentId });
Puntos clave: strapi.documents(uid) reemplaza strapi.entityService (eliminado en v5), documentId es el identificador persistente entre locales y versiones de borrador/publicado, el estado por defecto es 'draft' (pasa status: 'published' explícitamente), métodos dedicados publish()/unpublish()/discardDraft(). Consulta examples/core.md para ejemplos completos de CRUD y publicación/despublicación.
Patrón 4: Controladores Personalizados
Extiende o reemplaza las acciones de controlador generadas automáticamente. Los controladores manejan la lógica de solicitud/respuesta y delegan en los servicios. Usa createCoreController de factories para heredar los ayudantes de sanitización.
// src/api/article/controllers/article.ts
export default factories.createCoreController(
"api::article.article",
({ strapi }) => ({
async find(ctx) {
await this.validateQuery(ctx);
const sanitizedQuery = await this.sanitizeQuery(ctx);
const { results, pagination } = await strapi
.service("api::article.article")
.find(sanitizedQuery);
const sanitizedResults = await this.sanitizeOutput(results, ctx);
return this.transformResponse(sanitizedResults, { pagination });
},
}),
);
Por qué es bueno: validateQuery + sanitizeQuery + sanitizeOutput aplican el acceso a campos basado en roles, transformResponse() envuelve la salida en un envoltorio { data, meta }
Consulta examples/backend.md para ejemplos completos de controladores que incluyen acciones personalizadas (findBySlug, incrementViews, search).
Patrón 5: Rutas Personalizadas
Define rutas personalizadas para exponer acciones de controlador personalizadas. Los archivos de rutas personalizadas se colocan junto al enrutador principal.
// src/api/article/routes/custom-article.ts
export default {
routes: [
{
method: "GET",
path: "/articles/slug/:slug",
handler: "api::article.article.findBySlug",
config: { auth: false },
},
],
};
Por qué es bueno: Archivo separado del enrutador principal, handler usa el UID completo, auth: false para acceso público, políticas/middlewares adjuntables por ruta
Consulta examples/backend.md para restricción de rutas con only, adjuntar middlewares y configuración de políticas.
Patrón 6: Hooks de Ciclo de Vida
Registra efectos secundarios en las operaciones de tipos de contenido. Los hooks se definen en archivos lifecycles.ts junto al esquema del tipo de contenido.
// src/api/article/content-types/article/lifecycles.ts
export default {
async beforeCreate(event) {
/* event.params.data -- mutar para transformar la entrada */
},
async afterCreate(event) {
/* event.result -- el documento creado */
},
async beforeUpdate(event) {
/* event.params.data -- mutar antes de guardar */
},
async afterDelete(event) {
/* event.result -- limpiar recursos relacionados */
},
};
Por qué es bueno: Los hooks se activan automáticamente en las operaciones del Servicio de Documentos, beforeXxx puede mutar event.params.data, afterXxx tiene event.result
Nota: Para preocupaciones transversales en v5 (auditoría, invalidación de caché), prefiere el middleware del Servicio de Documentos (strapi.documents.use((ctx, next) => { ... })) registrado en src/index.ts. Los hooks de ciclo de vida siguen estando disponibles para efectos secundarios específicos de tipos de contenido.
Consulta examples/backend.md para ejemplos completos de ciclo de vida que incluyen generación de slugs, suscripción programática y registro de auditoría.
Patrón 7: Servicios
Los servicios contienen lógica de negocio reutilizable llamada por los controladores. Usa createCoreService para heredar el CRUD por defecto y agregar métodos personalizados.
export default factories.createCoreService(
"api::article.article",
({ strapi }) => ({
async findPublished(filters = {}) {
return strapi
.documents("api::article.article")
.findMany({ status: "published", filters });
},
}),
);
// Llamado a través de: strapi.service("api::article.article").findPublished()
Por qué es bueno: Hereda el CRUD por defecto, los métodos personalizados encapsulan lógica de consulta reutilizable, se llama desde los controladores a través de strapi.service(uid)
Consulta examples/backend.md para ejemplos de servicios complejos que incluyen consultas de artículos relacionados y patrones de publicación con notificación.
Patrón 8: Políticas
Las políticas son funciones de solo lectura que permiten o deniegan el acceso a una ruta. Devuelven true (permitir) o false (denegar) y no pueden modificar la solicitud.
// src/api/article/policies/is-owner.ts
export default async (policyContext, config, { strapi }) => {
const article = await strapi.documents("api::article.article").findOne({
documentId: policyContext.params.documentId,
populate: { author: true },
});
return article?.author?.documentId === policyContext.state.user?.documentId;
};
Por qué es bueno: Verificación de solo lectura (sin mutación de la solicitud), se aplica por acción en la configuración del enrutador principal
Consulta examples/backend.md para la aplicación de políticas a rutas, políticas de limitación de velocidad y encadenamiento de múltiples políticas.
<decision_framework>
Marco de Decisión
API REST vs API de Servicio de Documentos
¿Dónde se ejecuta tu código?
+-- Lado del cliente (navegador, aplicación frontend)
| +-- Usa la API REST (fetch /api/:pluralApiId)
+-- Lado del servidor (controlador personalizado, servicio, plugin, hook de ciclo de vida)
+-- Usa la API de Servicio de Documentos (strapi.documents(uid))
Estrategia de Población
¿Necesitas datos relacionados?
+-- NO --> No pases populate (respuesta más ligera)
+-- SÍ --> ¿Qué nivel de control?
+-- Todas las relaciones, 1 nivel --> populate: '*' (conveniente pero sobrecarga)
+-- Relaciones específicas --> populate: { relation: { fields: [...] } }
+-- Relaciones anidadas