Communitygithub.com

agents-inc/skills

開源無頭 CMS — 內容類型結構、REST API、文件服務 API、自訂控制器、生命週期鉤子、驗證、TypeScript

skills 是什麼?

這個技能(Skill)提供了在 Strapi v5 開源無頭 CMS 中建立、查詢和管理內容的完整指南。它詳細說明了從 `schema.json` 定義內容類型(Content Types)開始,到使用強制性的 Document Service API(`strapi.documents()`)進行後端資料存取,以及透過 `qs` 函式庫和 REST API 進行前端查詢的完整流程。此技能特別強調 Strapi v5 的關鍵變更:不再使用已棄用的 Entity Service,且 REST API 回應為扁平格式(`data.fieldName`),關聯與媒體資料預設不會被填充(需手動傳遞 `populate`)。此外,它還涵蓋了自訂控制器(Custom Controllers)、服務(Services)、路由(Routes)、策略(Policies)、中介軟體(Middlewares)以及生命週期鉤子(Lifecycle Hooks)的開發模式,並說明了如何透過使用者與權限外掛(Users & Permissions plugin)或 API Token 管理 JWT 驗證與權限。最後,此技能也包含了 TypeScript 型別生成的最佳實踐,確保開發者能構建安全、高效且可維護的 Strapi 應用程式。支援的平台包括 Strapi v5、Node.js(Koa)。

相容平台✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/agents-inc/skills/tree/HEAD/src/skills/api-cms-strapi

在你喜歡的 AI 中提問

開啟一個已預先載入此 Agent Skill 的新對話。

說明文件

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 中介軟體

驗證:


理念

Strapi 是一個基於 Node.js(Koa)的開源無頭 CMS,可根據內容類型結構自動生成 RESTful 和 GraphQL API。內容透過 JSON 結構定義、經由管理面板管理,並透過生成的 API 端點使用。

核心原則:

  1. 結構驅動內容 — 內容類型在 schema.json 檔案中定義,描述欄位、關聯、元件和動態區塊。管理面板的內容類型建構器提供了視覺化編輯器,但結構是存在於儲存庫中的程式碼。
  2. 自動生成 API — 每個內容類型會自動獲得 CRUD REST 端點(/api/:pluralApiId)和可選的 GraphQL 支援。標準操作無需手動建立路由/控制器。
  3. Document Service API(v5) — 用於從自訂程式碼、外掛和生命週期鉤子存取內容的後端 API。取代了 v4 的 Entity Service。使用 documentId(而非資料庫 id)作為主要識別碼。
  4. 草稿與發布 — 內容類型可以擁有草稿/發布流程。Document Service 預設使用 status: 'draft';已發布的內容需要 status: 'published' 或顯式呼叫 publish()。
  5. 權限優先 — 所有內容類型端點預設為私有。必須透過管理面板(「使用者與權限」外掛)或 API Token 明確授予存取權限。
  6. 後端自訂 — 控制器、服務、路由、策略和中介軟體都可以自訂。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>

Individual skills in this repo

This repo contains 11 individual skills — each has its own dedicated page.

相關技能