api-cms-strapi について
api-cms-strapi は、Strapi を使ったアプリ開発向けのスキルです。オープンソースのヘッドレス CMS を対象に、スキーマ主導のコンテンツ、生成 API、サーバー側の Document Service、バックエンドのカスタマイズを扱います。
主な機能
- 01コンテンツタイプのスキーマ設計
- 02qs による REST クエリ
- 03v5 の Document Service API
- 04カスタムコントローラーとルート
- 05認証と権限
- 06ライフサイクルフックとサービス
活用シーン
- 01コンテンツタイプとリレーションの定義
- 02フィルターや populate を使った REST API クエリ
- 03カスタムバックエンド処理の実装
- 04JWT アクセスと権限の設定
インストール
コマンドをコピーしてターミナルで実行します。
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 では、バックエンドのデータアクセスに、非推奨となった Entity Service ではなく Document Service API(
strapi.documents())を使用してください。REST API のレスポンスはフラットな形式です(data.attributes.fieldNameではなくdata.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 はデフォルトではリレーションを返しません)
(フィルター、populate、sort を含む複雑な REST API クエリー文字列の構築には、必ず qs ライブラリを使用してください。手動で文字列を構築すると、ネストされたパラメーターで問題が発生します)
(カスタムコントローラーでは、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)を定義する - フィルター、populate、sort、ページネーションを使って REST API にクエリーを送る
- カスタムコントローラー、サービス、ルート、ポリシー、ミドルウェアを作成する
- ドラフト/公開ワークフローでバックエンドの CRUD を行うために Document Service API を使用する
- Users & Permissions プラグインで JWT 認証を実装する
- 副作用を処理するため、コンテンツタイプにライフサイクルフックを追加する
- コンテンツスキーマの TypeScript 型を生成する
主な対象パターン:
- コンテンツタイプのスキーマ定義(
schema.json) qsを使った REST API クエリー(フィルター、populate、sort、ページネーション)- 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 API と GraphQL API を自動生成します。コンテンツは JSON スキーマで定義され、管理パネルで管理し、生成された API エンドポイントを通じて利用します。
主な原則:
- スキーマ駆動のコンテンツ -- コンテンツタイプは、フィールド、リレーション、コンポーネント、ダイナミックゾーンを記述する
schema.jsonファイルで定義します。管理パネルの Content-Type Builder はビジュアルエディターを提供しますが、スキーマはリポジトリに保存されるコードです。 - API の自動生成 -- すべてのコンテンツタイプに CRUD REST エンドポイント(
/api/:pluralApiId)が自動的に用意され、オプションで GraphQL にも対応します。標準的な操作であれば、ルートやコントローラーを手動で作成する必要はありません。 - Document Service API(v5) -- カスタムコード、プラグイン、ライフサイクルフックからコンテンツにアクセスするためのバックエンド API です。v4 の Entity Service に代わるもので、主識別子としてデータベースの
idではなくdocumentIdを使用します。 - 下書きと公開 -- コンテンツタイプでは、下書き/公開のワークフローを設定できます。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 にある 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 からスラッグを自動生成します。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";
// 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 を参照してください。
パターン 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)を使用してください。 - リレーションを populate しない — Strapi はデフォルトではリレーション、メディア、コンポーネント、ダイナミックゾーンを一切返しません。
populateを指定し忘れると、データが失われたように見える null/欠落フィールドが発生します。 - 本番環境で
populate=*を使用する — ユーザーに閲覧権限がない可能性のあるデータも含め、1 階層分のすべてのリレーションを取得します。フィールドを選択して対象を絞り込んだ populate を使用してください。 - クエリー文字列を手動で構築する — 複雑なフィルターや 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()に相当する処理を await しない — 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フィールド型にはスラッグが格納されます —uidフィールド型は、対象フィールドから URL セーフなスラッグを自動生成します。スラッグフィールドはトップレベルの属性として直接クエリーしてください(ネストされたアクセサーは不要です)。- 一括処理では Document Service からライフサイクルフックが実行されません —
beforeCreateMany、afterDeleteManyなどはデータベースレベルのフックです。Document Service の操作で実行されるのは、単一ドキュメント用のフックのみです。 - v5 のライフサイクルフックはデータベースレベルで実行されます — ライフサイクルフックはデータベース層で実行されるため、単一の Document Service 操作(例:
publish())によって、複数のデータベースレベルのフックが実行される場合があります。ロギング、検証、キャッシュ無効化などの横断的な関心事には、Document Service の抽象化レベルで動作する Document Service ミドルウェア(strapi.documents.use())を優先してください。 - ダイナミックゾーンの
populateにはon構文を使用します — ダイナミックゾーン内の特定のコンポーネントを populate するには、populate: { blocks: { on: { 'blocks.hero': { populate: '*' } } } }を使用してください。 - メディアフィールドは内部的にはリレーションです — メディアアップロードは
uploadプラグインに保存され、リレーション経由で参照されます。他のリレーションと同じ populate のルールに従います。 - 下書きの変更はデフォルトでは 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 はデフォルトではリレーションを返しません)
(フィルター、populate、sort を含む複雑な REST API クエリ文字列の構築には、必ず qs ライブラリを使用してください。文字列を手動で構築すると、ネストされたパラメーターで問題が発生します)
(カスタムコントローラーでは、this.sanitizeQuery(ctx)、this.sanitizeOutput()、this.validateQuery(ctx) を使用して、入力と出力の両方を必ずサニタイズおよび検証してください)
(管理パネルまたは設定で、すべてのコンテンツタイプのエンドポイントに権限を必ず設定してください。デフォルトでは、すべてのルートがプライベートです)
これらのルールに従わないと、データの欠落(populate の指定漏れ)、セキュリティ上の脆弱性(サニタイズの欠如)、クエリの破損(qs の未使用)、403 エラー(権限の未設定)が発生します。