GraphQL Code Generaror で CMS の API の型を生成する

はじめに

こんにちは。kimizuy です。

本記事ではGraphQL Code Generator を使ってクライアントサイドのリクエストとレスポンスの型定義を追加する方法を紹介します。

実際のプロダクトでも利用していますが、型安全にシステムを構築して保守性や堅牢性を高め、快適な開発環境を手に入れましょう!

前提

例で利用する API は DatoCMSContent Management API です。

あくまで例なので、本記事の内容は他の CMS の GraphQL API でも応用可能です。

また GraphQL クライアントには特に高機能なものは求めていなかったので、ミニマルかつシンプルに使える graphql-request を選びました。

GraphQL Code Generator をインストールする

Installation をもとに環境構築します。

yarn graphql-codegen init したりプラグインを追加したりして最終的に codegen.yml は以下のようになりました。

overwrite: true
schema: 'graphql/schema.graphql'
documents: 'graphql/**/*.graphql'
generates:
  graphql/generated/graphql.ts:
    plugins:
      - 'typescript'
      - 'typescript-operations'
      - 'typescript-graphql-request'
  ./graphql.schema.json:
    plugins:
      - 'introspection'

API Explorer から欲しい情報のクエリをコピーする

DatoCMS には API Explorer という GraphiQL のような GraphQL IDE が備わっており事前に API を叩いて試すことができます。

ここで実際に API を叩いて取得したいデータのクエリを取得します。

本記事では、以下のクエリをベースに型定義を生成します。$first の引数を与えることで任意の数の記事データを取得できます。$first: IntType = "3" で記事データを 3 つ取得します。

query getAllPosts($first: IntType = "3") {
  allPosts(first: $first) {
    slug
    title
    coverImage {
      url
    }
  }
}

以下はクエリの結果(レスポンス)です。

{
  "data": {
    "allPosts": [
      {
        "slug": "mistakes-tourists-make-on-their-first-trip-abroad",
        "title": "Mistakes Tourists Make on Their First Trip Abroad",
        "coverImage": {
          "url": "https://www.datocms-assets.com/57018/1585207275-image-33-copyright.jpg"
        }
      },
      {
        "slug": "spicy-jalapeno-bacon",
        "title": "How to Spend a Perfect Weekend Together",
        "coverImage": {
          "url": "https://www.datocms-assets.com/57018/1585207160-image-5-copyright.jpg"
        }
      },
      {
        "slug": "tips-on-how-to-see-more-and-stay-safe-in-asia",
        "title": "Tips on How to See More and Stay Safe in Asia",
        "coverImage": {
          "url": "https://www.datocms-assets.com/57018/1585207093-image-26-copyright.jpg"
        }
      }
    ]
  }
}

query.graphql にクエリを定義する

query.graphql という名前(任意)のファイルを用意してクエリを定義します。

ビックリマークをつけて IntType! とすることで必須の引数(つまり null にならない)になります。

# graphql/query.graphql
query getAllPosts($first: IntType!) {
  allPosts(first: $first) {
    slug
    title
    coverImage {
      url
    }
  }
}

schema.graphql を定義する

上記でクエリを定義したので、そこからスキーマを定義します。クエリとスキーマで不整合が起こると型の生成に失敗します(なので、まずは正しいクエリを定義しておくことを意識すると良さそうです)。

# graphql/schema.graphql
scalar IntType

type Image {
  url: String!
}

type Post {
  slug: String!
  title: String!
  coverImage: Image!
}

type Query {
  allPosts(first: IntType!): [Post!]!
}

DatoCMS では $first の引数は IntType という独自のスカラー型で定義されているので、これも定義しておく必要があります。

scalar IntType

このままでも良いですが、このスカラー型が TypeScript に変換されると any 型になります。

それを避けるには codegen.yml の config に型情報を追記します。

overwrite: true
schema: 'graphql/schema.graphql'
documents: 'graphql/**/*.graphql'
generates:
  graphql/generated/graphql.ts:
    plugins:
      - 'typescript'
      - 'typescript-operations'
      - 'typescript-graphql-request'
    config:
      scalars:
        IntType: string # 追加
        # StringFilter: '{eq: string}' クオートで囲めばオブジェクト型でも定義できる
  ./graphql.schema.json:
    plugins:
      - 'introspection'

参考: how to define custom scalars?

型を生成する

codegen.yml query.graphql schema.graphql をそれぞれ定義したので、型生成をする準備が整いました。

yarn graphql-codegen init をした際にスクリプトを追加したので、それを実行します(例: yarn generate)。

"generate": "graphql-codegen --config codegen.yml"

無事、生成に成功すると graphql/generated/graphql.ts に型が追加されます。

import { GraphQLClient } from 'graphql-request';
import * as Dom from 'graphql-request/dist/types.dom';
import gql from 'graphql-tag';
export type Maybe<T> = T | null;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
  ID: string;
  String: string;
  Boolean: boolean;
  Int: number;
  Float: number;
  IntType: string;
};

export type Image = {
  __typename?: 'Image';
  url: Scalars['String'];
};

export type Post = {
  __typename?: 'Post';
  coverImage: Image;
  slug: Scalars['String'];
  title: Scalars['String'];
};

export type Query = {
  __typename?: 'Query';
  allPosts: Array<Post>;
};


export type QueryAllPostsArgs = {
  first: Scalars['IntType'];
};

export type GetAllPostsQueryVariables = Exact<{
  first: Scalars['IntType'];
}>;


export type GetAllPostsQuery = { __typename?: 'Query', allPosts: Array<{ __typename?: 'Post', slug: string, title: string, coverImage: { __typename?: 'Image', url: string } }> };


export const GetAllPostsDocument = gql`
    query getAllPosts($first: IntType!) {
  allPosts(first: $first) {
    slug
    title
    coverImage {
      url
    }
  }
}
    `;

export type SdkFunctionWrapper = <T>(action: (requestHeaders?:Record<string, string>) => Promise<T>, operationName: string) => Promise<T>;


const defaultWrapper: SdkFunctionWrapper = (action, _operationName) => action();

export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper = defaultWrapper) {
  return {
    getAllPosts(variables: GetAllPostsQueryVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise<GetAllPostsQuery> {
      return withWrapper((wrappedRequestHeaders) => client.request<GetAllPostsQuery>(GetAllPostsDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'getAllPosts');
    }
  };
}
export type Sdk = ReturnType<typeof getSdk>;

実際に使ってみる

以下は Next.js 内で使ってみた一例です。getSdk(client).* やレスポンスデータの allPosts などで型が効きます。

import { GraphQLClient } from 'graphql-request'

const END_POINT = 'https://graphql.datocms.com/preview'

const client = new GraphQLClient(END_POINT, {
  headers: {
    Authorization: `Bearer ${process.env.NEXT_EXAMPLE_CMS_DATOCMS_API_TOKEN}`,
  },
})

const { allPosts } = await getSdk(client).getAllPosts({ first: '100' })

参考

おわりに

本記事では CMS の GraphQL API を利用した GraphQL Code Generator の導入について紹介しました。

クエリとスキーマを定義して型を生成する工程を挟むことで正しい型での開発に制約できるので、バグが入り込む余地を減らすことができます。

実際にプロダクトで利用してみて、レスポンスデータに型があることの快適さは想像以上でした。

小さなアプリでは不要な可能性はありますが、一定規模以上のアプリなら導入の検討を強くオススメします!

以上、お読みいただきありがとうございました。


投稿者 Yamasaki Kimizu

React, Redux, TypeScript プロジェクトでフロントエンド領域を担当。個人でも Next.js アプリの開発をしています。日課はRSSで取得した技術記事を読むこと、最近の関心は Core Web Vitals です。将来はでかい犬が飼いたいです。