オープンソースのヘッドレスCMS Strapi入門

# はじめに

Strapiは、JavaScriptで書かれたオープンソースのヘッドレスCMSです。RESTやGraphQLをサポートしており、どのようなクライアントからでもAPIを利用することができます。

:::note info
Strapiの発音
[Youtubeチャンネル](https://www.youtube.com/@Strapi)によると、Strapiの発音は「ストラピ」で良さそうです。
:::

<details><summary>Strapiを採用した理由</summary>

学校の課題で観光アプリを開発することになったのですが、期限やメンバーの言語的にバックエンドが厳しい状況でした。

Strapiなら簡単にAPIを実装できるため採用しました。
</details>


この記事では、新規Strapiプロジェクトの作成からはじまり、アプリケーションのコンテンツを構築していきます。最終的には、REST APIの設定を行い、コンテンツのCRUD操作を行います。

# Strapi環境構築

## 環境

- Windows 11 バージョン 22H2
- Node v18.15.0
- npm 9.6.3
- Strapi 4.11.3

## 前提条件

執筆当時の2023年7月現在、Strapiをインストールするためには、以下の要件がインストールされている必要があります。

- Node.js: Strapi `v4.3.9`以上では、Node `v18.x`推奨
- Node.jsパッケージマネージャー
    - npm
    - yarn
- Python （データベースにSQLiteを使用する場合）

また、サポートされているデータベースは以下の通りです。

- MySQL: `5.7.8`以上、`8.0`以降推奨
- MariaDB: `10.3`以上、`10.6`以降推奨
- PostgreSQL: `11.0`以上、`14.0`以降推奨
- SQLite: `3`以上

## 新規Strapiプロジェクトの作成

ターミナルで以下のコマンドを実行してください。

```bash
$ yarn create strapi-app <プロジェクト名>
# または
$ npx create-strapi-app@latest <プロジェクト名>
# 以降はnpmコマンドのみ掲載します
```

上記のコマンドを実行すると、インストールタイプの選択を求められます。`Quickstart`と`Custom`があり、前者を選択するとSQLiteデータベースが採用され、後者を選択するとお好みの設定でサポートされているデータベースから選ぶことができます。

```bash
$ npx create-strapi-app@latest my-project
? Choose your installation type
  Quickstart (recommended)
> Custom (manual settings)
? Choose your preferred language
  JavaScript
> TypeScript
? Choose your default database client
  sqlite
  postgres
> mysql
? Database name: (my-project)    
? Host: (127.0.0.1) 
? Port: (3306) 
? Username:
? Password:
? Enable SSL connection: (y/N) 

Creating a project with custom database options.
```

また、StrapiのCLIにはいくつかインストールオプションが用意されています。
例えば、Quickstartタイプ（SQLiteデータベース）かつTypeScriptでプロジェクトを作成したい場合は以下のようになります。

```bash
# QuickstartタイプかつTypeScriptでStrapiプロジェクトを作成する
$ npx create-strapi-app@latest my-project --quickstart --ts
```

## 管理者ユーザーの登録

インストールが完了すると、ブラウザは新しいタブを開き、管理者ユーザーの情報入力を求められます。

![strapi-register-admin.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594132/c8316ada-8837-55f8-5d5a-492c99ef0045.png)

フォームに必要な情報を入力すると、管理者ユーザーのアカウントが作成されます。これにてStrapiの管理画面にアクセスできるようになり、新規Strapiプロジェクトの作成は完了です。

## （オプション） Strapiの日本語化

Strapiは、（一部）管理画面の日本語化に対応しています。オプションとしたのは、「一部」とあるように、完全な日本語化には公式では対応していないためです。

まずは、`src/admin/app.example.(js|tsx)`ファイルをコピペして、`app.(js|tsx)`ファイルとしてリネームします。そして、`config.locales`配列内にある`ja`のコメントアウトを外します。

```tsx:src/admin/app.tsx
export default {
  config: {
    locales: [
      'ja',
    ],
  },
  bootstrap(app) {
    console.log(app);
  },
};
```

そして、ここで1度`strapi build`を行い、管理UIの更新を行います。

```bash
$ npm run build
```

ビルドが完了したら再度Strapiを動かします。

```bash
$ npm run develop
```

アカウントをクリック→Profileと進むとユーザープロフィール画面が表示されます。その中にExperienceという項目があるので、そのInterface Languageというフォームで日本語を選択し、変更を保存します。

すると、管理画面が一部日本語化されます。

完全な日本語化を行いたい場合は、何らかの日本語化プラグインをインストールすると良いかもしれません。

# コンテンツの構築

ここでは、学校の課題と同じく観光アプリを例にコンテンツを構築していきたいと思います。

まずは観光アプリに必要な、観光スポット、カテゴリー、そしてユーザーが投稿可能なコメントのコレクションを作成していきます。

コレクションの作成は、管理画面の左サイドバーにあるContent-Type Builderから行うことができます。COLLECTION TYPESタブ内のCreate new collection typeをクリックすることで、新規コレクションを作成することができます。

![strapi-content-type-builder-how-to-use.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594132/c2fd85c8-8b08-1695-3b89-af25dc36edee.png)

## 観光スポットコレクションの作成

Create new collection typeをクリックすると表示される、Configurationsではコレクションの基本的な情報を設定します。

ここでは、

- Display name: 観光スポット
- API ID（Singular）: sightseeing-spot
- API ID（Plural）: sightseeing-spots
- Draft & publish（高度な設定）: チェックを外す

として、Continueをクリックします。なおAPI IDですが、この2つの項目はDisplay nameを英語で入力すると自動で入力されます。今回は日本語で表示したいため、一度英語で入力してからAPI IDをコピーしておき、Display nameを日本語に修正しました。

続いて、この観光スポットコレクションが保持するフィールドの追加を行います。

- name: Text, 必須フィールド（高度な設定）
- description: Text, Long text
- photos: Media, Multiple media

## カテゴリーコレクションの作成

ここではカテゴリーコレクションの作成を行います。
カテゴリーは1つの観光スポットに対して複数個登録することができます。例えば、Qiitaアミューズメントホテルは宿泊カテゴリーとアミューズメントカテゴリーの両方を併せ持つようなケースがあります。

上記のリレーションをもとにカテゴリーコレクションの設定を行っていきます。

- Display name: カテゴリー
- API ID（Singular）: category
- API ID（Plural）: categories
- Draft & publish（高度な設定）: チェックを外す

続いて、カテゴリーコレクションが保持するフィールドの追加です。

- name: Text, 必須フィールド（高度な設定）
- sightseeingSpots: Relation, カテゴリー has and belongs to many 観光スポット, categories

## コメントコレクションの作成

コメントコレクションの作成も行います。

コメントは、観光スポットに対してユーザーが投稿を行います。ここではデモとして、コメントの更新と削除が可能とします。

- Display name: コメント
- API ID（Singular）: comment
- API ID（Plural）: comments
- Draft & publish（高度な設定）: チェックを外す

コメントコレクションが保持するフィールドの追加も行います。

- name: Text, デフォルト値 匿名希望さん（高度な設定）
- message: Text, Long text, 必須フィールド（高度な設定）, 最大長 255（高度な設定）
- sightseeingSpot: Relation, 観光スポット has many コメント, comments

## コンテンツの追加

必要なコレクションの作成が完了しました。上記のステップで作成したコレクションに対して、テスト用にコンテンツを追加していきます。

コンテンツの追加は、管理画面の左サイドバーにあるContent Managerから行うことができます。COLLECTION TYPESタブからコレクションを選択し、Create new entryボタンを押すことでコンテンツを追加することができます。

![strapi-content-manager-example.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594132/6c41954d-5287-9e9f-9e4e-3724de6e018b.png)

テスト用に何件かデータを登録してみました。

![strapi-test-data.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594132/51fe8cc2-875e-00bb-701b-2f9aac4bab62.png)

# REST API

## APIトークンの作成

クライアントからStrapiのAPIを呼び出すには、APIトークンを用意する必要があります。

管理画面の左サイドバーにあるSettingsから、グローバル設定タブのAPIトークンという項目を選択し、エントリを追加ボタンを押してAPIトークンの作成を行います。

![strapi-create-api-token-page.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594132/4e0468e3-a766-b306-ce98-409462bf0540.png)

まずDetailsにAPIトークンの情報を入力します。NameとToken durationは任意の値を入力し、Token typeはCustomを選択します。

続いて、Permissionsの設定です。ここではコレクションごとに許可するCRUDを選択します。以下の項目にチェックを入れてください。

- Category: find
- Comment: find, create, update, delete
- Sightseeing-spot: find, findOne

:::note info
CategoryとCommentでfindを許可している理由
観光スポット取得時に、カテゴリーとコメントのリレーションも一緒に取得したいため、CategoryとCommentでもfindを許可する必要があります。
:::

Permissionsの設定を終えたら、画面右上にあるSaveボタンを押すと、APIトークンが画面に表示されます。ここで表示されているAPIトークンは画面を移動すると消えてしまうため、忘れないようにメモしておく必要があります。

## 観光スポット一覧の取得

前回のセクションで追加した観光スポットコレクションの一覧を取得してみましょう。以下のURLにGETリクエストを送ることで、コレクションの一覧を取得することができます。

なお、Authorizationの`{{token}}`の部分には、APIトークンの作成時に表示されていたAPIトークンが入ります。

```
GET /api/sightseeing-spots
Authorization: Bearer {{token}}
```

以下のようなレスポンスが返ってきます。

```json
{
  "data": [
    {
      "id": 1,
      "attributes": {
        "name": "Qiitaアミューズメントホテル",
        "description": null,
        "createdAt": "2023-07-06T13:00:05.013Z",
        "updatedAt": "2023-07-06T13:13:23.382Z"
      }
    },
    {
      "id": 2,
      "attributes": {
        "name": "Qiita博物館",
        "description": "Qiitaの歴史がつまった博物館です。",
        "createdAt": "2023-07-06T13:00:16.912Z",
        "updatedAt": "2023-07-06T13:03:10.269Z"
      }
    },
    {
      "id": 3,
      "attributes": {
        "name": "Qiitaパーク",
        "description": null,
        "createdAt": "2023-07-06T13:00:29.679Z",
        "updatedAt": "2023-07-06T13:00:29.679Z"
      }
    }
  ],
  "meta": {
    "pagination": {
      "page": 1,
      "pageSize": 25,
      "pageCount": 1,
      "total": 3
    }
  }
}
```

## 観光スポットの取得

観光スポット一覧の取得にて、`categories`や`photos`が取得されていないことに注目してください。Strapiでは、RelationやMediaなどのフィールドはデフォルトでは取得されない設定になっています。

このようなフィールドも取得したい場合は、`populate`パラメータをリクエストに追加する必要があります。

例えば、IDが1の観光スポットのカテゴリーとコメントも取得したい場合は、以下のようなリクエストを送る必要があります。

```
GET /api/sightseeing-spots/1?populate=categories,comments
Authorization: Bearer {{token}}
```

このGETリクエストのレスポンスは以下のようになります。

```json
{
  "data": {
    "id": 1,
    "attributes": {
      "name": "Qiitaアミューズメントホテル",
      "description": null,
      "createdAt": "2023-07-06T13:00:05.013Z",
      "updatedAt": "2023-07-06T13:13:23.382Z",
      "categories": {
        "data": [
          {
            "id": 1,
            "attributes": {
              "name": "アミューズメント",
              "createdAt": "2023-07-06T12:58:54.491Z",
              "updatedAt": "2023-07-06T12:58:54.491Z"
            }
          },
          {
            "id": 2,
            "attributes": {
              "name": "宿泊",
              "createdAt": "2023-07-06T12:59:11.015Z",
              "updatedAt": "2023-07-06T12:59:11.015Z"
            }
          }
        ]
      },
      "comments": {
        "data": []
      }
    }
  },
  "meta": {}
}
```

`populate`パラメータを追加することで、categoriesフィールドとcommentsフィールドも取得することができました。

Strapiでは、`populate`以外にもページネーション用の`pagination`や、レスポンスフィルタリング用の`filters`などが利用可能です。より詳細な情報が必要な場合は、[REST API parameters](https://docs.strapi.io/dev-docs/api/rest/parameters)から確認することができます。

<details><summary>カテゴリーがアミューズメントまたは宿泊の観光スポット一覧を取得したい場合</summary>

例えばこのようなケースのパラメータは以下の通りです。

```json
/api/sightseeing-spots?filters[categories][id][$in][0]=2&filters[categories][id][$in][1]=3&populate=*
```

パラメータが複雑になってきましたね。Strapiでは、[Query Builder](https://docs.strapi.io/dev-docs/api/rest/interactive-query-builder)が用意されているので、こちらを活用するという方法もあります。
</details>

## コメントの追加

続いて、コメントコレクションにコンテンツを追加してみたいと思います。

コメントを追加するには、以下のようなJSON形式でPOSTリクエストを送る必要があります。（更新用にあえてタイプミスしています…）

```
POST /api/comments
Authorization: Bearer {{token}}
Content-Type: application/json
{
    "data": {
        "message": "良いですｎ!",
        "sightseeingSpot": 1
    }
}
```

以下のようなレスポンスが返ってくると思います。

```json
{
  "data": {
    "id": 1,
    "attributes": {
      "name": "匿名希望さん",
      "message": "良いですｎ!",
      "createdAt": "2023-07-06T13:36:36.207Z",
      "updatedAt": "2023-07-06T13:36:36.207Z"
    }
  },
  "meta": {}
}
```

なお、messageフィールドは必須フィールドとして設定したため、messageフィールドをコメントアウトしてPOSTリクエストを送ると、以下のようなエラーレスポンスが返ってきます。

```json
{
  "data": null,
  "error": {
    "status": 400,
    "name": "ValidationError",
    "message": "message must be defined.",
    "details": {
      "errors": [
        {
          "path": [
            "message"
          ],
          "message": "message must be defined.",
          "name": "ValidationError"
        }
      ]
    }
  }
}
```

## コメントの更新

先ほどタイプミスしたコメントを修正してみます。

```
PUT /api/comments/1
Authorization: Bearer {{token}}
Content-Type: application/json
{
    "data": {
        "message": "良いですね!"
    }
}
```

```json
{
  "data": {
    "id": 1,
    "attributes": {
      "name": "匿名希望さん",
      "message": "良いですね!",
      "createdAt": "2023-07-06T13:36:36.207Z",
      "updatedAt": "2023-07-06T13:39:40.475Z"
    }
  },
  "meta": {}
}
```

Content Manager画面のリロードや観光スポットの再取得を行うと、先ほど作成したコメントのタイプミスが修正されていることが確認できると思います。

## コメントの削除

最後にコメントの削除を行ってみたいと思います。

```
DELETE /api/comments/1
Authorization: Bearer {{token}}
```

削除に成功すると、以下のように削除されたデータそのものがレスポンスとして返ってきます。

```json
{
  "data": {
    "id": 1,
    "attributes": {
      "name": "匿名希望さん",
      "message": "良いですね!",
      "createdAt": "2023-07-06T13:36:36.207Z",
      "updatedAt": "2023-07-06T13:39:40.475Z"
    }
  },
  "meta": {}
}
```

# おわりに

今回の記事ではStrapi入門と題して、新規Strapiプロジェクトの作成からコンテンツ構築、そしてREST APIによるコンテンツのCRUD操作を行いました。

StrapiにはカスタムプラグインやGraphQLサポートなど今回紹介しきれていない豊富な機能があります。今後も学習を続けてStrapiに詳しくなったら入門編の次、中級編でも書いてみたいと思います。