Strapi 模式
快速指南: 將 Strapi 作為開源無頭 CMS 使用,從內容類型結構自動生成 REST/GraphQL API。在 v5 中,請使用 Document Service API(
strapi.documents())進行後端資料存取,而非已棄用的 Entity Service。REST API 回應使用扁平格式(data.fieldName,而非data.attributes.fieldName)。關聯與媒體資料預設不會被填充 — 請務必傳遞populate。使用qs構建複雜的查詢字串。內容類型預設為私有;需透過「使用者與權限」外掛或 API Token 設定權限。
<critical_requirements>
關鍵:使用此技能前請先閱讀
所有程式碼必須遵循 CLAUDE.md 中的專案慣例(kebab-case、具名匯出、匯入排序、
import type、具名常數)
(你必須使用 strapi.documents('api::content-type.content-type')(Document Service API)進行 Strapi v5 的所有後端資料存取 — 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
- 建立自訂控制器、服務、路由、策略或中介軟體
- 使用 Document Service API 進行後端 CRUD 操作並搭配草稿/發布流程
- 使用「使用者與權限」外掛實作 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檔案中定義,描述欄位、關聯、元件和動態區塊。管理面板的內容類型建構器提供了視覺化編輯器,但結構是存在於儲存庫中的程式碼。 - 自動生成 API — 每個內容類型會自動獲得 CRUD REST 端點(
/api/:pluralApiId)和可選的 GraphQL 支援。標準操作無需手動建立路由/控制器。 - Document Service API(v5) — 用於從自訂程式碼、外掛和生命週期鉤子存取內容的後端 API。取代了 v4 的 Entity Service。使用
documentId(而非資料庫id)作為主要識別碼。 - 草稿與發布 — 內容類型可以擁有草稿/發布流程。Document Service 預設使用
status: 'draft';已發布的內容需要status: 'published'或顯式呼叫publish()。 - 權限優先 — 所有內容類型端點預設為私有。必須透過管理面板(「使用者與權限」外掛)或 API Token 明確授予存取權限。
- 後端自訂 — 控制器、服務、路由、策略和中介軟體都可以自訂。Strapi 遵循類似 MVC 的模式,並建構於 Koa 之上。
核心模式
模式 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),目標化的 populate 搭配 fields 可避免過度擷取(生產環境絕不使用 populate=*),動態區塊元件使用 on 語法。完整的過濾、填充和分頁範例請參閱 examples/core.md。
模式 3:Document Service API(後端)
Document Service API 用於自訂控制器、服務、生命週期鉤子和外掛中,以便從伺服器端存取內容。它取代了 v4 的 Entity Service(已在 v5 中移除)。
const CONTENT_TYPE_UID = "api::article.article";
// 尋找已發布的內容(預設為 'draft')
const articles = await strapi.documents(CONTENT_TYPE_UID).findMany({
status: "published",
filters: { categories: { slug: { $eq: "news" } } },
populate: { author: true },
});
// CRUD 使用 documentId(而非資料庫 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 -- 變更以轉換輸入 */
},
async afterCreate(event) {
/* event.result -- 已建立的 document */
},
async beforeUpdate(event) {
/* event.params.data -- 在儲存前變更 */
},
async afterDelete(event) {
/* event.result -- 清理相關資源 */
},
};
為什麼好: 鉤子在 Document Service 操作時自動觸發,beforeXxx 可以變更 event.params.data,afterXxx 可以存取 event.result
注意: 對於 v5 中的橫切關注點(稽核日誌、快取失效),建議使用註冊在 src/index.ts 的 Document Service 中介軟體(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 });
},
}),
);
// 透過以下方式呼叫: 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
你的程式碼在哪裡執行?
+-- 客戶端(瀏覽器、前端應用程式)
| +-- 使用 REST API (fetch /api/:pluralApiId)
+-- 伺服器端(自訂控制器、服務、外掛、生命週期鉤子)
+-- 使用 Document Service API (strapi.documents(uid))
填充策略
你需要相關資料嗎?
+-- 不需要 --> 不要傳遞 populate(較精簡的回應)
+-- 需要 --> 你需要什麼程度的控制?
+-- 所有關聯,1 層 --> populate: '*'(方便但會過度擷取)
+-- 特定關聯 --> populate: { relation: { fields: [...] } }
+-- 嵌套關聯
…
[已截斷以利翻譯]
</decision_framework>