Strapi 模式
快速指南: 使用 Strapi 作为开源无头 CMS,从内容类型架构自动生成 REST/GraphQL API。在 v5 中,使用文档服务 API (
strapi.documents()) 进行后端数据访问,而非已弃用的 Entity Service。REST API 响应采用扁平格式(data.fieldName,而不是data.attributes.fieldName)。默认情况下,关系和媒体不会被填充——始终传递populate。使用qs构建复杂查询字符串。内容类型默认是私有的;通过用户权限插件或 API 令牌配置权限。
<critical_requirements>
关键:使用此技能之前
所有代码必须遵循 CLAUDE.md 中的项目约定(kebab-case、命名导出、导入排序、
import type、命名常量)
(在 Strapi v5 中,你必须使用 strapi.documents('api::content-type.content-type')(文档服务 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::, lifecycle hooks, Users & Permissions, /api/auth/local, populate, qs.stringify
何时使用:
- 构建以 Strapi 作为无头 CMS 的内容管理应用程序
- 定义包含字段、关系、组件和动态区段的内容类型架构(
schema.json) - 使用过滤器、填充、排序和分页查询 REST API
- 创建自定义控制器、服务、路由、策略或中间件
- 使用文档服务 API 进行后端 CRUD 操作,支持草稿/发布工作流
- 使用用户权限插件实现 JWT 身份验证
- 为内容类型添加生命周期钩子以实现副作用
- 为内容架构生成 TypeScript 类型
涵盖的关键模式:
- 内容类型架构定义(
schema.json) - 使用
qs进行 REST API 查询(过滤器、填充、排序、分页) - 文档服务 API(
findMany、findOne、create、update、delete、publish、unpublish) - 自定义控制器、服务、路由、策略和中间件
- 生命周期钩子(
beforeCreate、afterUpdate等) - JWT 身份验证(注册、登录、经过身份验证的请求)
- TypeScript 类型生成
何时不使用:
- 非 Strapi 的 CMS 平台(使用专用于您的 CMS 的技能)
- 绕过 Strapi API 层的直接数据库查询(使用 Strapi 的文档服务)
- 需要原始 SQL 的复杂事务逻辑(Strapi 抽象了数据库)
详细资源:
- 有关决策框架和快速参考表格,请参阅 reference.md
核心与 REST API:
- examples/core.md —— 内容类型架构、REST API 查询、文档服务 API、错误处理
后端定制:
- examples/backend.md —— 自定义控制器、服务、路由、策略、中间件、生命周期钩子、文档服务中间件
身份验证:
- examples/auth.md —— JWT 身份验证、注册、登录、角色和权限
理念
Strapi 是一个基于 Node.js (Koa) 的开源无头 CMS,它从内容类型架构自动生成 RESTful 和 GraphQL API。内容通过 JSON 架构定义,通过管理面板管理,并通过生成的 API 端点消费。
核心原则:
- 架构驱动的内容 —— 内容类型在
schema.json文件中定义,这些文件描述字段、关系、组件和动态区段。管理面板的内容类型构建器提供了可视化编辑器,但架构是驻留在您仓库中的代码。 - 自动生成的 API —— 每个内容类型都会自动获得 CRUD REST 端点(
/api/:pluralApiId)和可选的 GraphQL 支持。标准操作无需手动创建路由/控制器。 - 文档服务 API (v5) —— 用于从自定义代码、插件和生命周期钩子访问内容的后端 API。取代了 v4 的 Entity Service。使用
documentId(而不是数据库id)作为主标识符。 - 草稿与发布 —— 内容类型可以具有草稿/发布工作流。文档服务默认使用
status: 'draft';已发布内容需要status: 'published'或显式调用publish()。 - 权限优先 —— 所有内容类型端点默认是私有的。必须通过管理面板(用户权限插件)或 API 令牌显式授予访问权限。
- 后端定制 —— 控制器、服务、路由、策略和中间件都可以自定义。Strapi 遵循基于 Koa 的 MVC 类似模式。
核心模式
模式 1:内容类型架构
内容类型在 ./src/api/[api-name]/content-types/[content-type-name]/schema.json 的 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:文档服务 API (后端)
文档服务 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 */
},
};
为什么好: 钩子在文档服务操作时自动触发,beforeXxx 可以修改 event.params.data,afterXxx 有 event.result
注意: 对于 v5 中的横切关注点(审计日志、缓存失效),更推荐使用在 src/index.ts 中注册的文档服务中间件(strapi.documents.use((ctx, next) => { ... }))。生命周期钩子仍然可用于特定内容类型的副作用。
有关包含 slug 生成、程序化订阅和审计日志的完整生命周期示例,请参阅 examples/backend.md。
模式 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 与文档服务 API
您的代码在哪里运行?
+-- 客户端(浏览器、前端应用)
| +-- 使用 REST API (fetch /api/:pluralApiId)
+-- 服务器端(自定义控制器、服务、插件、生命周期钩子)
+-- 使用文档服务 API (strapi.documents(uid))
填充策略
您是否需要相关数据?
+-- 否 --> 不传递 populate(响应更精简)
+-- 是 --> 需要什么级别的控制?
+-- 所有关系,1 层 --> populate: '*'(方便但过度获取)
+-- 特定关系 --> populate: { relation: { fields: [...] } }
+-- 嵌套关系
…
[为翻译截断]
</decision_framework>