2023.02.20[Mon]

Strapi v4のAPIをカスタムして扱いやすくする

  • Strapi

目次

こんにちは。キューでWebエンジニアをしている永井です。

今回は、Strapi v4のAPIを扱いやすくする方法を紹介します。
Strapi v4ではAPI周りに大きな変更がありましたが、v3と比べると扱いにくくなりました。

簡単にあげると以下のような点です。

  • レスポンスの階層が深くなり、必要なデータにアクセスしづらい
  • 関連テーブルのデータ取得にクエリを指定する必要がある
  • カウントAPIが削除された

こちらを修正して扱いやすくしていきます。

前提

・Strapi v4のバックエンド環境がある

バックエンドの実装

コントローラーの編集

対象APIのコントローラーファイル(/api/[apiName]/controllers/[apiName].js)を編集します。
コントローラーファイルを編集することで、既存APIの動作を変更することができます。

「一覧取得(find)」と「詳細取得(findOne)をオーバーライドしてレスポンスを変更します。
全体のコードはこちらです。

"use strict";

const { createCoreController } = require("@strapi/strapi").factories;

module.exports = createCoreController("api::xxx.xxx", () => ({
  async find(ctx) {
    ctx.query = { ...ctx.query, populate: "*" };

    const { data } = await super.find(ctx);

    return toV3Data(data, ctx.query);
  },

  async findOne(ctx) {
    if (ctx.request.url.includes("/count")) {
      const {
        meta: {
          pagination: { total },
        },
      } = await super.find(ctx);
      return total;
    }

    ctx.query = { ...ctx.query, populate: "*" };

    const { data } = await super.findOne(ctx);

    return toV3Data(data, ctx.query);
  },
}));

/**
 * strapi v4のレスポンスをv3の形に変換する
 */
const toV3Data = (data, query) => {
  if (data == null) return null;

  if (data instanceof Array) {
    return data.map((d) => toV3Data(d, query)).filter((d) => d);
  }

  return {
    id: data.id,
    ...Object.entries(data.attributes).reduce((acc, [key, value]) => {
      if (key === "createdAt" || key === "updatedAt" || key === "publishedAt") {
        key = key.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
      }
      if (value != null && typeof value === "object" && "data" in value) {
        value = toV3Data(value.data, query);
      }
      return { ...acc, [key]: value };
    }, {}),
  };
};

解説

クエリの追加

v4から関連テーブルのデータを取得するには、populateを指定する必要があります。
今回はデフォルトで全て取得したいので、populate: "*"を指定します。
全て取得したくない(クエリで制御したい)場合は、特に追加する必要はありません。

カウントの追加

v3にあった/[apiName]/countのAPIはv4で削除されました。カウントは一覧取得のメタ情報から取得できますが、APIとして用意されていた方が便利なので追加します。

追加方法としては2つあります。

  1. findOneの関数内に処理を追加する方法
    /[apiName]/countを呼び出すと、fineOneのURLパターン/[apiName]/:idに引っかかりfindOneが実行されてしまいます。そこで、findOneの関数内でidが"count"だった場合の処理を追加します。
  2. カスタムルーターを作成する方法
    APIにカスタムルーターを作成してルーティングを変更します。例えば、findOneに向いているルーティングを別途用意したcountの関数に向けることができます。

今回はお手軽に実装できるfindOneの関数内に処理を追加する方法を採用しています。
カスタムルーターの詳細はこちらから確認できます。

v3変換用に関数を追加

v4のレスポンスのデータ構造を、v3のデータ構造に変換する関数を追加します。
v4のデータはdataattributesといった階層が増えてとても扱いづらいです。
なので、そちらを削除してv3の時と同じデータにします。

・変換前のデータ(v4データ)

data: [
  {
    id: 1,
    attributes: {
      title: "Test",
      content: "Test content",
      publishedAt: "2023-01-30T09:24:58.606Z",
      createdAt: "2023-01-30T09:24:03.890Z",
      updatedAt: "2023-01-30T09:24:58.615Z",
      tags: {
        data: [
          {
            id: 1,
            attributes: {
              name: "Test Tag",
              publishedAt: "2023-01-30T09:25:54.720Z",
              createdAt: "2023-01-30T09:23:50.468Z",
              updatedAt: "2023-01-30T09:25:54.741Z"
            },
          },
        ],
      },
    },
  },
]

・変換後のデータ(v3と同等)

[
  {
    id: 1,
    title: "Test",
    content: "Test content",
    published_at: "2023-01-30T09:24:58.606Z",
    created_at: "2023-01-30T09:24:03.890Z",
    updated_at: "2023-01-30T09:24:58.615Z",
    tags: [
      {
        id: 1,
        name: "Test Tag",
        published_at: "2023-01-30T09:25:54.720Z",
        created_at: "2023-01-30T09:23:50.468Z",
        updated_at: "2023-01-30T09:25:54.741Z"
      }
    ]
  }
]

処理をまとめる

APIが複数ある場合、同じ処理を毎回書くのは面倒なのでまとめます。
Strapiの処理をまとめる方法は、以前書いたこちらの記事を参考に進めます。

ファイルの作成

処理をまとめる用のファイルを/configに作成します。
ファイル名の指定はないので、functions.jsとしておきます。

ファイルの編集

コントローラーファイルに実装していた処理をこちらのファイルに移動します。
findfindOneはコントローラーファイルから呼び出す必要があるのでエクスポートします。

module.exports = {
  convertToV3Data: () => ({
    async find(ctx) {
      ctx.query = { ...ctx.query, populate: "*" };

      const { data } = await super.find(ctx);

      return toV3Data(data, ctx.query);
    },

    async findOne(ctx) {
      if (ctx.request.url.includes("/count")) {
        const {
          meta: {
            pagination: { total },
          },
        } = await super.find(ctx);
        return total;
      }

      ctx.query = { ...ctx.query, populate: "*" };

      const { data } = await super.findOne(ctx);

      return toV3Data(data, ctx.query);
    },
  }),
};

const toV3Data = (data, query) => {
...
};

コントローラーから呼び出す

最後にコントローラーから/config/function.jsに定義した関数を呼び出せば完了です。

"use strict";

const { createCoreController } = require("@strapi/strapi").factories;

module.exports = createCoreController("api::xxx.xxx", strapi.config.functions.convertToV3Data);

まとめ

Strapi v4のAPIを扱いやすくする方法を紹介しました。
バックエンド側に手を加えることで、フロント側はシンプルに実装することができるのでおすすめです。

参考
https://docs.strapi.io/developer-docs/latest/update-migration-guides/migration-guides/v4/code-migration.html

Share

Shopify Post-Purchase Extensionでレジ前販売を実装する 機能拡張編③ 〜ディスカウント・確認〜Shopify Post-Purchase Extensionでレジ前販売を実装する 機能拡張編② 〜複数商品〜