👨🏼‍💻

GraphQL のデータソースに microCMS を 使う

2024/03/03に公開

はじめに

この記事では、GraphQL のデータソースに microCMS を利用する方法を書いてみます。GraphQL は BFF として利用するケースも多いですが、GraphQL としては、データがどこから来たのかは重要ではありません。データベースや RESTful API、マイクロサービスから取得するといった選択肢があります。今回は microCMS をデータ取得元にすることを想定し、その構成や構築手順を書いていきます。(以下の図の REST API の部分が mciroCMS になるイメージです。)

GraphQL is data source agnostic


出典: https://www.apollographql.com/blog/graphql/basics/what-is-graphql-introduction/

なぜ microCMS を使いたいのか

microCMS を GraphQL のデータソースとして利用したいケースとしては、GraphQL のデータソースが複数あり、microCMS で管理するコンテンツとそれ以外のデータソースから取得したデータを連携させたい、といったケースを想定しています。

例. データソースに microCMS(REST API) と Database があり、microCMS のコンテンツの情報と合わせて Database で管理するデータを一緒に返したい。

利用するライブラリ

利用するライブラリを紹介します。GraphQL Server に graphql-yoga、コードを自動生成するツールに gcg-typescript-resolver-files と microcms_sdk_generator を使います。それぞれ GraphQL のスキーマ、microCMS の API スキーマから、コードを自動生成します。

graphql-yoga
GraphQL Server

https://github.com/dotansimha/graphql-yoga

gcg-typescript-resolver-files
GraphQL Server 側のコードを自動生成するプラグインです。

https://github.com/eddeee888/graphql-code-generator-plugins/tree/master/packages/typescript-resolver-files

microcms_sdk_generator
microCMS のクライアント側のコードを自動生成するツールです。

https://github.com/hori-ryota/microcms_sdk_generator

graphql-yoga の環境を用意する

まずは GraphQL Yoga を使って GraphQL サーバーの環境を用意していきます。

1. GraphQL Yoga とは
graphql-yoga は、多くの GraphQL 関連の OSS をメンテナンスしてる The Guild が作っている OSS で、 GraphQL Server の機能を提供します。
細かいスペックの紹介は、公式のページに譲りますが、Nodejs、Deno、Cloudflare Workers など様々ランタイムで動かせること、プラグインのエコシステムが充実してるなどの特徴があります。

https://the-guild.dev/graphql/yoga-server/docs/comparison

2. 環境を用意する方法
公式のチュートリアルがわかりやすいので、手元で環境を構築する細かい手順はチュートリアルのページを参照すると良さそうです。一通り流すと手元で動く環境を用意できます。

https://the-guild.dev/graphql/yoga-server/tutorial/basic

3. GraphQL Yoga の推しポイント
個人的に、GraphQL Code Generator をはじめ、GraphQL 全体のエコシステムの中心に The Guild があるような印象を受けています。このグループから提供されてる安心感、ドキュメントのわかりやすさ、既に多くのプラグインが用意されてる便利さなどが、推しポイントです。

https://the-guild.dev/about-us

gcg-typescript-resolver-files を導入する

続いて、gcg-typescript-resolver-files を導入していきます。

1. gcg-typescript-resolver-files とは
GraphQL のスキーマから、規約に従って resolver のコードを自動生成するプラグインです。
以下のようにモジュール単位でディレクトリを切り、それぞれにスキーマを配置して、graphql-codegen --config ./codegen.ts を実行すると、モジュールに分けた単位で resolver のコードが自動生成されます。

├── src/
│   ├── schema/
│   │   ├── base/
│   │   │   ├── schema.graphql
│   │   ├── user/
│   │   │   ├── schema.graphql
│   │   ├── book/
│   │   │   ├── schema.graphql

自動生成された後のファイル構成は以下のようになります。

├── src/
│   ├── schema/
│   │   ├── base/
│   │   │   ├── schema.graphql
│   │   ├── user/
│   │   │   ├── resolvers/
│   │   │   │   ├── Query/
│   │   │   │   │   ├── user.ts            # Generated, changes not overwritten by codegen
│   │   │   │   ├── User.ts                # Generated, changes not overwritten by codegen
│   │   │   ├── schema.graphql
│   │   ├── book/
│   │   │   ├── resolvers/
│   │   │   │   ├── Query/
│   │   │   │   │   ├── book.ts            # Generated, changes not overwritten by codegen
│   │   │   │   ├── Mutation/
│   │   │   │   │   ├── markBookAsRead.ts  # Generated, changes not overwritten by codegen
│   │   │   │   ├── Book.ts                # Generated, changes not overwritten by codegen
│   │   │   ├── schema.graphql
│   │   ├── resolvers.generated.ts         # Entirely generated by codegen
│   │   ├── typesDefs.generated.ts         # Entirely generated by codegen
│   │   ├── types.generated.ts             # Entirely generated by codegen

自動生成されたコードは以下のようになっていて、開発者がやらないければいけない作業は、未実装のロジックの部分を埋めればいいだけ、という感じになっています。

import type { QueryResolvers } from "./../../../types.generated";
export const user: NonNullable<QueryResolvers["user"]> = async (
  _parent,
  _arg,
  _ctx
) => {
  /* Implement Query.user resolver logic here */
  // ここにロジックを実装するのみ
};

2. 導入方法
導入方法は公式のガイドがわかりやすいので、詳細な手順は公式ガイドを参照ください。graphql-yoga の tutorial を流した環境で以下のページのガイドを手順通り試すことで、 gcg-typescript-resolver-files を導入できます。

https://the-guild.dev/graphql/codegen/docs/guides/graphql-server-apollo-yoga-with-server-preset

3. gcg-typescript-resolver-files の推しポイント
resolver が返さないといけない型も GraphQL のスキーマから自動生成されたコードに割り当てられます。残りの実装はパズルのようにロジック部分を書くだけなので、開発体験がすごく良かったです。

ライブラリ作者の気持ちとしては、API 開発をスケールさせるためにこのプラグインを作っているようです。プロダクトコードがスケールする際発生する様々な問題に対処すべく、例えば、コード生成による規約を作ったり、モジュールごとにオーナーシップを持って管理できるようにモジュールが分かれる構成を強制するなどの解決策を提供します。詳細は以下の記事が参考になるのでおすすめです。

https://the-guild.dev/blog/scalable-apis-with-graphql-server-codegen-preset

microcms_sdk_generator を導入する

続いては、microcms_sdk_generator を導入していきます。

1. microcms_sdk_generator とは
microCMS の API スキーマの json から client のコードを自動生成するツールです。作者によるツールの紹介記事が、作った背景、導入方法など、わかりやすくておすすめです。

https://hori-ryota.com/blog/create-microcms-sdk-generator/

2. 導入方法

インストールします。

$ pnpm i microcms_sdk_generator
$ pnpm i zod

microCMS の管理画面から API スキーマを json 形式のファイルでエクスポートできるので取得します。

例. microCMS の管理画面からエクスポートしたスキーマファイル blog.json の詳細

以下の形式の json になっています。

{
  "apiFields": [
    {
      "idValue": "7EwsUooATH",
      "fieldId": "title",
      "name": "タイトル",
      "kind": "text",
      "required": true,
      "isUnique": false
    },
    {
      "fieldId": "category",
      "name": "カテゴリー",
      "kind": "relation",
      "required": true
    },
    {
      "fieldId": "author",
      "name": "著者",
      "kind": "relation",
      "required": true
    },
    { "fieldId": "image", "name": "画像", "kind": "media" },
    {
      "fieldId": "body",
      "name": "本文",
      "kind": "richEditorV2",
      "required": true
    }
  ],
  "customFields": []
}

取得方法は microCMS のドキュメントを参照ください。
https://document.microcms.io/manual/export-and-import-api-schema

取得したファイルを以下のようなディレクトリ構成で配置します。
ここで配置するファイル名は、microCMS の API 名と合わせる必要があります。(例. blog の API スキーマだったら、blog.json にする)

# ディレクトリ名にルールはありません。任意の名前のディレクトリを作成してください。
 $ mkdir ./micro-cms-schemas
 $ tree ./micro-cms-schemas
 ./micro-cms-schemas
 ├── list
 │   ├── author.json # microCMSからエクスポートしたAPIスキーマを配置する
 │   ├── blog.json # microCMSからエクスポートしたAPIスキーマを配置する
 │   └── category.json # microCMSからエクスポートしたAPIスキーマを配置する
 └── object

ファイルの準備ができたら、ツールのコマンドを実行します。

$ npx microcms_sdk_generator ./micro-cms-schemas ./micro-cms-schemas/generated.ts

コマンドを実行すると、generated.ts が自動生成されます。

 $ mkdir ./micro-cms-schemas
 $ tree ./micro-cms-schemas
 ./micro-cms-schemas
 ├── generated.ts # ファイルが自動生成される
 ├── list
 │   ├── author.json
 │   ├── blog.json
 │   └── category.json
 └── object

generated.ts にはスキーマの型定義や API を叩く関数などが用意されます。抜粋すると以下のようなコードが生成されます。(例. Blog スキーマの型定義)

~省略;
export const BlogDefSchema = z.object({
  title: z.string(),
  category: OnlyIdSchema.and(z.unknown()),
  author: OnlyIdSchema.and(z.unknown()),
  image: z
    .object({
      url: z.string().url(),
      height: z.number(),
      width: z.number(),
    })
    .optional(),
  body: z.string(),
});
export type BlogDef = z.infer<typeof BlogDefSchema>;

~省略;

export const BlogOutputSchema = makeListResponseSchema(BlogDefSchema);

~省略;

export const BlogSchema = BlogOutputSchema.shape.contents.element;

~省略;

export type Blog = z.infer<typeof BlogSchema>;

3. microcms_sdk_generator の推しポイント

これらの型やスキーマ定義のメンテナンスは API が増えてくるとそれなりに時間的なコストを費やすことになるのですが、そもそもスキーマ定義間のマッピングは機械的に解釈できるものであり、人間がやる必要はないのでツールによる自動化を図りました。

作者の方の記事にも書かれていますが、microCMS の API が返すレスポンスに型をつけるのは結構大変な作業なので、API スキーマの定義から型定義が自動で生成されるのはとても便利です。

GraphQL の型と microCMS の型をマッピングさせロジックを実装する

ここまでで、GraphQL Yoga の構築、コード生成ツールの導入ができました。最後に、GraphQL の型と microCMS の型をマッピングさせ、ロジックの実装を仕上げます。

gcg-typescript-resolver-files には、リゾルバーが最終的に返す型(スキーマ)と他のインターフェースをマッピングさせるマッパーを追加する機能があります。
例えば、GraphQL の Blog スキーマが返す型と microCMS の Blog API スキーマが返す型をマッピングできます。この際、以下のように schema.mappers.ts を用意します。

// 例. apps/backend/src/schema/blog/schema.mappers.ts
// サンプル実装: https://github.com/shimabukuromeg/graphql-yoga-sample/blob/main/apps/backend/src/schema/blog/schema.mappers.ts
import { Blog } from "micro-cms-schemas/generated";

export type Blog_Mapper = Blog;

https://the-guild.dev/graphql/codegen/docs/guides/graphql-server-apollo-yoga-with-server-preset#adding-mappers

GraphQL スキーマの型と microCMS の型をマッピングさせることで、マッピングした GraphQL スキーマのフィールドについては、microCMS から取得したデータをそのまま返すことができます。リゾルバー側で必須で返さないといけないフィールドだが、マッパー側(microCMS 側)で持っていないフィールドは、追加でリゾルバーに実装する必要があります。

以下、GraphQL Blog スキーマのサンプルコードです。

GraphQL Blog スキーマ

# 例. apps/backend/src/schema/blog/schema.graphql
# サンプル実装: https://github.com/shimabukuromeg/graphql-yoga-sample/blob/main/apps/backend/src/schema/blog/schema.graphql
extend type Query {
  blog(id: ID!): Blog!
}

"""
ブログ記事
"""
type Blog {
  id: ID!
  title: String!
  body: String!
  image: MicroCmsImage
  category: Category!
  author: Author!
  createdAt: DateTime!
  updatedAt: DateTime!
  publishedAt: DateTime
  revisedAt: DateTime
}

Blog Query リゾルバー
microCms.blog.get は microcms_sdk_generator で生成したブログ一覧を取得するためのクライアントのコードです。

// apps/backend/src/schema/blog/resolvers/Query/blog.ts
// サンプル実装: https://github.com/shimabukuromeg/graphql-yoga-sample/blob/main/apps/backend/src/schema/blog/resolvers/Query/blog.ts
import type { QueryResolvers } from "./../../../types.generated";
export const blog: NonNullable<QueryResolvers["blog"]> = async (
  _parent,
  arg,
  ctx
) => {
  const { data } = await ctx.microCms.blog.get({
    id: arg.id,
  });
  return data;
};

Blog Object リゾルバー
author と category のフィールドは microCMS 側の型定義には存在しないフィールドですが、Blog リゾルバーとしては必要なフィールドなので、以下のようにして追加で実装します。(Object リゾルバーは最終的にクラアントに返す値となります。)

// apps/backend/src/schema/blog/resolvers/Blog.ts
// サンプル実装: https://github.com/shimabukuromeg/graphql-yoga-sample/blob/main/apps/backend/src/schema/blog/resolvers/Blog.ts
import type { BlogResolvers } from "./../../types.generated";
export const Blog: BlogResolvers = {
  /* Implement Blog resolver logic here */
  author: async (parent, arg, ctx) => {
    /* Blog.author resolver is required because Blog.author and Blog_Mapper.author are not compatible */
    const { data } = await ctx.microCms.author.get({
      id: parent.author.id,
    });
    return data;
  },
  category: async (parent, arg, ctx) => {
    const { data } = await ctx.microCms.category.get({
      id: parent.category.id,
    });
    return data;
  },
};

ここまで実装したら Yoga Server を起動して試してにクエリを叩いてみます。microCMS がデータソース元になっているクエリのデータ取得できるようになります。

ここまでで紹介してきたライブラリ・実装のサンプルは以下のリポジトリにまとまってます。

https://github.com/shimabukuromeg/graphql-yoga-sample

おわりに

以上、microCMS を GraphQL のデータソースとして利用する方法の紹介でした。実際に使う際は、N+1 やテストコードなど、いろいろと考慮しないといけないところがありそうです。そのあたりも、整理できたら記事にまとめていきたい。

参考

https://zenn.dev/ubie_dev/articles/b1c9258cbf3ceb

https://zenn.dev/shunjuio/articles/e5515872871534

GitHubで編集を提案
株式会社モニクル

Discussion