关于 api-cms-strapi
api-cms-strapi 是用于使用 Strapi 开发应用的技能;Strapi 是开源无头 CMS。内容涵盖模式驱动的内容、自动生成的 API、服务端 Document Service 用法及后端定制。
主要功能
- 01内容类型模式设计
- 02使用 qs 构建 REST 查询
- 03v5 Document Service API
- 04自定义控制器与路由
- 05身份验证与权限
- 06生命周期钩子与服务
适用场景
- 01定义内容类型与关系
- 02按筛选和填充参数查询 REST API
- 03实现自定义后端逻辑
- 04配置 JWT 访问与权限
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ agents-inc/ skills --skill api-cms-strapi安装后在 Claude Code 中输入/api-cms-strapi确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
metadata.yamlreference.mdexamples/auth.mdbackend.mdcore.md详细介绍
Strapi 模式
快速指南: 将 Strapi 用作开源无头 CMS,根据内容类型架构自动生成 REST/GraphQL API。在 v5 中,使用 Document Service API(
strapi.documents())访问后端数据,而不要使用已弃用的 Entity Service。REST API 响应采用扁平格式(data.fieldName,而不是data.attributes.fieldName)。默认不会填充关联和媒体——始终传入populate。使用qs构建复杂的查询字符串。内容类型默认是私有的;通过 Users & Permissions 插件或 API 令牌配置权限。
<critical_requirements>
重要:使用此技能前
所有代码都必须遵循项目在 CLAUDE.md 中的约定(kebab-case、命名导出、导入顺序、
import type、命名常量)
(在 Strapi v5 中,所有后端数据访问都必须使用 strapi.documents('api::content-type.content-type')(Document Service API)——Entity Service API 已被移除)
(需要关联、媒体、组件或动态区域时,必须始终传入 populate——Strapi 默认不返回任何关联)
(必须使用 qs 库构建包含筛选、填充和排序的复杂 REST API 查询字符串——手动拼接字符串会导致嵌套参数出错)
(在自定义控制器中,必须使用 this.sanitizeQuery(ctx)、this.sanitizeOutput() 和 this.validateQuery(ctx) 清理并验证输入和输出)
(必须通过管理面板或配置为每个内容类型端点设置权限——所有路由默认都是私有的)
</critical_requirements>
自动检测: Strapi、strapi、@strapi/strapi、createCoreController、createCoreService、createCoreRouter、Document Service、strapi.documents、content-type、schema.json、api::、plugin::、生命周期钩子、Users & Permissions、/api/auth/local、populate、qs.stringify
适用场景:
- 使用 Strapi 作为无头 CMS 构建内容管理应用
- 定义包含字段、关联、组件和动态区域的内容类型架构(
schema.json) - 使用筛选、填充、排序和分页查询 REST API
- 创建自定义控制器、服务、路由、策略或中间件
- 使用 Document Service API,通过草稿/发布工作流执行后端 CRUD 操作
- 使用 Users & Permissions 插件实现 JWT 身份验证
- 为内容类型添加生命周期钩子以执行副作用
- 为内容架构生成 TypeScript 类型
涵盖的关键模式:
- 内容类型架构定义(
schema.json) - 使用
qs查询 REST API(筛选、填充、排序、分页) - Document Service API(
findMany、findOne、create、update、delete、publish、unpublish) - 自定义控制器、服务、路由、策略和中间件
- 生命周期钩子(
beforeCreate、afterUpdate等) - JWT 身份验证(注册、登录、已验证请求)
- TypeScript 类型生成
不适用场景:
- 非 Strapi 的 CMS 平台(使用对应 CMS 的专用技能)
- 绕过 Strapi API 层直接查询数据库(使用 Strapi 的 Document Service)
- 需要原始 SQL 的复杂事务逻辑(Strapi 对数据库进行了抽象)
详细资源:
- 决策框架和速查表请参阅 reference.md
核心与 REST API:
- examples/core.md -- 内容类型架构、REST API 查询、Document Service API、错误处理
后端自定义:
- examples/backend.md -- 自定义控制器、服务、路由、策略、中间件、生命周期钩子、Document Service 中间件
身份验证:
- examples/auth.md -- JWT 身份验证、注册、登录、角色和权限
## 理念
Strapi 是一个基于 Node.js(Koa)构建的开源无头 CMS,可根据内容类型架构自动生成 RESTful 和 GraphQL API。内容通过 JSON 架构定义,经由管理面板管理,并通过生成的 API 端点访问。
核心原则:
- 架构驱动的内容 —— 内容类型定义在
schema.json文件中,用于描述字段、关系、组件和动态区域。管理面板中的 Content-Type Builder 提供可视化编辑器,但架构本质上是存储在代码仓库中的代码。 - 自动生成 API —— 每种内容类型都会自动获得 CRUD REST 端点(
/api/:pluralApiId)和可选的 GraphQL 支持。标准操作无需手动创建路由或控制器。 - Document Service API(v5) —— 用于在自定义代码、插件和生命周期钩子中访问内容的后端 API。它取代了 v4 的 Entity Service。它使用
documentId(而非数据库id)作为主要标识符。 - 草稿与发布 —— 内容类型可采用草稿/发布工作流。Document Service 默认使用
status: 'draft';已发布内容需要设置status: 'published'或显式调用publish()。 - 权限优先 —— 所有内容类型端点默认均为私有。必须通过管理面板(Users & Permissions 插件)或 API 令牌明确授予访问权限。
- 后端自定义 —— 控制器、服务、路由、策略和中间件都可自定义。Strapi 遵循基于 Koa 构建的 MVC 类模式。
核心模式
模式 1:内容类型架构
内容类型定义在 ./src/api/[api-name]/content-types/[content-type-name]/schema.json 文件中。多文档内容(文章、产品)使用 collectionType,单文档内容(站点设置、首页)使用 singleType。
{
"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"]
}
}
}
关键字段: uid 根据 targetField 自动生成 slug,relation 类型通过 inversedBy/mappedBy 定义基数,component 嵌入可复用区块,dynamiczone 支持混合组件类型。完整的集合类型和单一类型示例请参阅 examples/core.md。
模式 2:使用 qs 查询 REST API
REST API 支持复杂的查询参数,可用于筛选、填充、排序和分页。始终使用 qs 库构建查询字符串。
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();
要点: encodeValuesOnly 可避免括号编码问题;筛选器使用运算符($eq、$notNull、$containsi、$or、$and);使用带 fields 的定向 populate 可避免过度获取数据(生产环境中切勿使用 populate=*);动态区域组件使用 on 语法。完整的筛选、填充和分页示例请参阅 examples/core.md。
模式 3:Document Service API(后端)
Document Service API 用于在自定义控制器、服务、生命周期钩子和插件中从服务器端访问内容。它取代了 v4 的 Entity Service(已在 v5 中移除)。
const CONTENT_TYPE_UID = "api::article.article";
// Find published content (default is 'draft')
const articles = await strapi.documents(CONTENT_TYPE_UID).findMany({
status: "published",
filters: { categories: { slug: { $eq: "news" } } },
populate: { author: true },
});
// CRUD uses documentId (not database id)
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 });
要点: strapi.documents(uid) 取代了 strapi.entityService(已在 v5 中移除);documentId 是跨语言环境及草稿/已发布版本的持久标识符;默认状态为 'draft'(需显式传入 status: 'published');提供专用的 publish()/unpublish()/discardDraft() 方法。完整的 CRUD 和发布/取消发布示例请参阅 examples/core.md。
模式 4:自定义控制器
扩展或替换自动生成的控制器操作。控制器负责处理请求/响应逻辑,并将工作委托给服务。使用 factories 中的 createCoreController 来继承清理辅助方法。
// 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 });
},
}),
);
优点: validateQuery + sanitizeQuery + sanitizeOutput 可强制执行基于角色的字段访问;transformResponse() 会将输出封装在 { data, meta } 信封中。
完整的控制器示例(包括自定义操作 findBySlug、incrementViews、search)请参阅 examples/backend.md。
模式 5:自定义路由
定义自定义路由,以公开自定义控制器操作。自定义路由文件与核心路由器放在一起。
// src/api/article/routes/custom-article.ts
export default {
routes: [
{
method: "GET",
path: "/articles/slug/:slug",
handler: "api::article.article.findBySlug",
config: { auth: false },
},
],
};
优点: 与核心路由器分开存放,handler 使用完整 UID,通过 auth: false 允许公开访问,可为每条路由附加策略和中间件
有关使用 only 限制路由、附加中间件和配置策略,请参阅 examples/backend.md。
模式 6:生命周期钩子
为内容类型操作注册副作用。钩子定义在与内容类型架构放在一起的 lifecycles.ts 文件中。
// src/api/article/content-types/article/lifecycles.ts
export default {
async beforeCreate(event) {
/* event.params.data -- mutate to transform input */
},
async afterCreate(event) {
/* event.result -- the created document */
},
async beforeUpdate(event) {
/* event.params.data -- mutate before save */
},
async afterDelete(event) {
/* event.result -- cleanup related resources */
},
};
优点: 钩子会在 Document Service 操作时自动触发,beforeXxx 可以修改 event.params.data,afterXxx 可以获取 event.result
注意: 在 v5 中,对于横切关注点(如审计日志记录、缓存失效),建议使用在 src/index.ts 中注册的 Document Service 中间件(strapi.documents.use((ctx, next) => { ... }))。生命周期钩子仍可用于内容类型特定的副作用。
有关生命周期示例,请参阅 examples/backend.md,其中包括生成 slug、以编程方式订阅和审计日志记录。
模式 7:服务
服务包含可由控制器调用的可复用业务逻辑。使用 createCoreService 继承默认 CRUD 功能并添加自定义方法。
export default factories.createCoreService(
"api::article.article",
({ strapi }) => ({
async findPublished(filters = {}) {
return strapi
.documents("api::article.article")
.findMany({ status: "published", filters });
},
}),
);
// Called via: strapi.service("api::article.article").findPublished()
优点: 继承默认 CRUD 功能,自定义方法封装可复用的查询逻辑,可通过 strapi.service(uid) 从控制器调用
有关复杂服务示例,请参阅 examples/backend.md,其中包括相关联文章查询和发布时发送通知的模式。
模式 8:策略
策略是只读函数,用于允许或拒绝对路由的访问。它们返回 true(允许)或 false(拒绝),且不能修改请求。
// 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;
};
优点: 只读检查(不会修改请求),可在核心路由器配置中按操作应用
有关将策略应用于路由、速率限制策略和多策略链式调用,请参阅 examples/backend.md。
<decision_framework>
决策框架
REST API 与 Document Service API
Where is your code running?
+-- Client-side (browser, frontend app)
| +-- Use REST API (fetch /api/:pluralApiId)
+-- Server-side (custom controller, service, plugin, lifecycle hook)
+-- Use Document Service API (strapi.documents(uid))
填充策略
Do you need related data?
+-- NO --> Don't pass populate (leaner response)
+-- YES --> What level of control?
+-- All relations, 1 level --> populate: '*' (convenient but over-fetches)
+-- Specific relations --> populate: { relation: { fields: [...] } }
+-- Nested relations --> populate: { relation: { populate: { nested: true } } }
+-- Filtered relations --> populate: { relation: { filters: { ... } } }
内容类型种类
How many documents of this type exist?
+-- Many (articles, products, users) --> collectionType
+-- One (site settings, homepage, footer) --> singleType
自定义控制器与默认控制器
Do default CRUD endpoints meet your needs?
+-- YES --> Use auto-generated routes (no custom controller needed)
+-- NO --> What do you need?
+-- Modified default behavior --> Override find/findOne/create/update/delete in createCoreController
+-- Entirely new endpoint --> Add custom action + custom route
+-- Access control logic --> Add a policy to the route config
+-- Request transformation --> Add a route middleware
草稿与发布
Does content need editorial review before going live?
+-- YES --> Enable draftAndPublish: true in schema
| +-- Document Service defaults to status: 'draft'
| +-- Use publish()/unpublish() to manage lifecycle
| +-- REST API returns published content by default
+-- NO --> Disable draftAndPublish (content is always live)
身份验证方式
Who is consuming the API?
+-- End users (login/register) --> Users & Permissions plugin (JWT)
+-- External services/scripts --> API tokens (admin panel > Settings > API Tokens)
+-- Admin panel users --> Admin API tokens (separate from content API)
</decision_framework>
<red_flags>
危险信号
高优先级问题:
- 在 v5 中使用
strapi.entityService——Entity Service 已移除。所有后端数据访问都应使用strapi.documents()(Document Service API)。 - 未填充关联数据——Strapi 默认不会返回关联、媒体、组件或动态区域。忘记使用
populate会导致字段为 null 或缺失,看起来像是数据丢失。 - 在生产环境中使用
populate=*——会获取一层深度内的所有关联,其中可能包含用户无权查看的数据。请使用指定字段的定向填充。 - 手动构造查询字符串——手动构建复杂的筛选/填充 URL 会因特殊字符和嵌套参数而出错。请使用
qs库。 - 自定义控制器中缺少清理操作——跳过
sanitizeQuery()、sanitizeOutput()和validateQuery()会绕过权限检查,并暴露私有字段。
中优先级问题:
- 混淆
documentId与id——在 v5 中,documentId是跨语言区域和草稿/已发布版本的持久标识符。数据库id是内部整数。REST API 和 Document Service 使用documentId。 - 未设置权限——默认情况下,所有内容类型端点都是私有的。若未在管理面板中配置权限,API 请求会返回 403。
- 假设默认状态为已发布——Document Service API 默认使用
status: 'draft'。若要获取已发布内容,必须显式传入status: 'published'。 - 使用
publicationState参数(v4 语法)——v5 中已由status参数以及专用的publish()/unpublish()方法取代。 - 在
qs.stringify中忘记encodeValuesOnly: true——若不设置此选项,qs会对数组方括号索引进行编码,而 Strapi 的解析器可能无法正确处理。
常见错误:
- 未等待相当于
.commit()的操作——Document Service 方法会返回 Promise。务必对其使用await。 - 组件中缺少
_type或_key——通过 API 创建/更新动态区域和组件字段时,需要使用__component标识符。 - 将 Strapi URL 硬编码——API 基础 URL 应使用环境变量(
STRAPI_URL);硬编码localhost:1337会导致生产环境出错。 - 混用分页方法——只能使用
page/pageSize或start/limit,不要在同一查询中同时使用两者。
注意事项与边缘情况:
- v5 响应格式为扁平结构——v4 将数据嵌套在
data.attributes中;v5 则将字段直接放在 data 对象上。现有的 v4 前端代码会因此出错。 uid字段类型存储 slug——uid字段类型会根据目标字段自动生成 URL 安全的 slug。可直接将 slug 字段作为顶层属性查询(无需嵌套访问器)。- Document Service 不会触发批量生命周期钩子——
beforeCreateMany、afterDeleteMany等属于数据库级钩子。Document Service 操作只会触发单文档钩子。 - v5 中的生命周期钩子属于数据库级别——生命周期钩子在数据库层触发,因此单个 Document Service 操作(例如
publish())可能会触发多个数据库级钩子。对于日志记录、验证和缓存失效等横切关注点,建议使用 Document Service 中间件(strapi.documents.use()),它在 Document Service 抽象层运行。 - 动态区域的
populate使用on语法——若要填充动态区域中的特定组件,请使用populate: { blocks: { on: { 'blocks.hero': { populate: '*' } } } }。 - 媒体字段在内部作为关联处理——媒体上传存储在
upload插件中,并通过关联引用。它们遵循与其他关联相同的填充规则。 - 默认情况下,REST API 不会显示草稿更改——REST API 返回已发布内容。若要查看草稿,需要具有相应权限的令牌以及
status=draft参数。
</red_flags>
<critical_reminders>
关键提醒
所有代码都必须遵循 CLAUDE.md 中的项目规范(kebab-case、命名导出、导入排序、
import type、命名常量)
(在 Strapi v5 中,所有后端数据访问都必须使用 strapi.documents('api::content-type.content-type')(Document Service API)——Entity Service API 已移除)
(需要关联、媒体、组件或动态区域时,必须始终传入 populate——Strapi 默认不会返回任何关联)
(必须使用 qs 库构建包含筛选条件、populate 和排序的复杂 REST API 查询字符串——手动拼接字符串会破坏嵌套参数)
(在自定义控制器中,必须使用 this.sanitizeQuery(ctx)、this.sanitizeOutput() 和 this.validateQuery(ctx) 清理并验证输入与输出)
(必须通过管理面板或配置为每个内容类型端点设置权限——默认情况下,所有路由都是私有的)
不遵循这些规则会导致数据缺失(未使用 populate)、安全漏洞(未清理数据)、查询失效(未使用 qs)以及 403 错误(未设置权限)。
</critical_reminders>