21
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

GraphQLAdvent Calendar 2019

Day 24

TypeScript + GraphQLで変更作業をできるだけ減らしたい

Last updated at Posted at 2019-12-23

GraphQL Advent Calendar 2019 24日目です。
今年も残りわずかになってきましたね!一年がすぎるのは本当にはやい!

変更箇所多い問題

TypeScript + GraphQLで開発していて、例えば新しくフィールドを追加したくなったりするかもしれません。
その場合の流れは以下のようになると思います。

  1. データベースにカラムを追加する
  2. ORMにフィールドを追加する
  3. リソルバーにフィールドを追加する
  4. ORM経由で追加したフィールドも取得するようにする
  5. フロントエンドのクエリにフィールドを追加する
  6. クエリの返り値の型を変更する

一つのフィールドを追加するだけで変更する箇所が多すぎてやる気が失せてきますね... :crying_cat_face:

でも、ここ一年でTypeScript + GraphQLの開発がApolloやPrisma等のツールによって、
自分で型を定義する必要性も減ったりと、より便利になってきたなと感じています。

今回はTypeScript + GraphQLでフロントエンドからバックエンドまでエンドツーエンドでどのように開発するのかを紹介します!

エンドツーエンドな開発

以下の図はTypeScript + GraphQLによるエンドツーエンドでの開発の概要を示しています。
E2E GraphQL (1).png

それぞれのコンポーネントに関する説明は後でするとして、簡単に開発フローを説明します。

サーバーサイド

  1. GraphQL NexusでGraphQL API (クエリー、ミューテーション、オブジェクトなど全て)をTypeScriptで記述する。
  2. 1で作成したTypeScriptのコードからGraphQL APIで用いる型定義ファイルを生成する。
  3. DBのデータモデル定義ファイル(prisma.schema)を記述し、Photon.jsが用いるクエリエンジンを生成する。NexusからPhoton.js経由でDBにアクセスする。
  4. Apollo ServerにはNexusより生成したスキーマを渡す。

フロントエンド

  1. GraphQLドキュメントを書く。例えば以下のような感じ。
 const GET_UESR_QUERY = gql`
   query getUser($id: ID!){
     user(id: $id) {
       id
       username
       age
     }
   }
 `
  1. graphql-code-generatorでGraphQLドキュメントからReact Apolloで用いる型ファイルを生成する、もしくはHooksを生成する。
  2. あとはReact Apolloで上記の型やHooksを使うだけ。

各コンポーネントの詳細

ここからは各コンポーネントをもう少し詳しく説明していきます。

GraphQL Nexus

エンドツーエンドの開発で一番コアとなるのはGraphQL Nexusです。
Nexusは「宣言的でコードファーストなJavaScript/TypeScript向け」のGraphQL APIライブラリです。1

  • TypeGraphQL
    こちらはNexus同様コードファーストですが、大きな違いはデコレータを使用していることです。
    また、NestJSというNode.jsのサーバーサイドフレームワークはGraphQLをTypedGraphQLを通じてサポートしています。
  • graphql-code-generator
    スキーマファースト。様々なプラグインが用意されており、フロントエンドで用いるコードも生成することができます(後述)。
    prismaはgraphqlgenというスキーマファーストなツールを開発していましたが、deprecatedとなりました

NexusでGraphQLのオブジェクトそしてそれらの紐づくリゾルバーを書いたら、TypeScriptの型定義ファイルを自動生成します。

Photon.js

Photon.jsはPrismaが開発しているTypeScriptに対応したORMです。
現在はまだアルファ版で来年のQ1にはproduction readyになるようです。
アルファ版なのでまだバグや機能的な制限もあります。
例えばクエリにマッチした行数をカウントする関数がなかったりします。一般的なORMではカウントの機能はちゃんとカバーされていると思います。

TypeScriptのORMといえばTypeORMが有名だと思うのですが、
自分のプロジェクトで使うにあたり機能的な制限 (ネストクエリが使えなかいなど)があったので使っていません。
総合的に見てTypeORMのほうがコミュニティも大きいし開発年数も長いので信頼できるとは思うのですが、
Prismaが今後Typescript + GraphQLでのエコシステムをより改善していくことを期待してPhotonを使っています。
また、MikroORMというORMもあり、ウォッチしています
(デコレータを使うところが個人的にあんまり好きではないです)。

graphql-code-generator

graphql-code-generatorはGraphQLスキーマやドキュメントからTypeScriptの型定義ファイルやReact Hooksなどを生成してくれます。

React Apolloが提供するコンポーネントはgqlなどのGraphQLドキュメントをパースしたものを入力として受け取りますが、
GraphQLドキュメントを文字列として与えても取得されるデータや変数の型を推論してくれるみたいなことはありません(あったらめっちゃ便利ですけど)。
ですのでGraphQLドキュメントからこれらの型を生成するためのツールとしてgraphql-code-generatorを使っています。
graphql-code-generatorでGraphQLドキュメントを監視し、変更があった際は、
Apollo Serverから取得したGraphQLスキーマをもとにTypeScriptの型ファイルを自動生成します。
GraphQLスキーマと整合性のないドキュメントであった場合はエラーが出るようになっているのでめっちゃ便利です。
あとはこの生成された型ファイルをReact Apolloで使えば、フロントエンドでもTypeScriptな開発ができます。

ちなみにサーバーサイド側ではコードファーストなのに、フロントエンド(クライアント)はスキーマ (ドキュメント)ファーストになっていることのお気づきの方もいらっしゃるのではないかと思います。
クライアント側でもtyped-graphqlifyというツールがあり、最初は使っていたのですが、verboseなところがありやはり直感的に書きたいよねっていうことでスキーマファーストにしました。

変更作業はどこまで減ったか?

  1. :writing_hand: Prismaスキーマファイル記述
  2. :robot: データベースにカラムを追加する (Prismaで自動追加)
  3. :robot: ORMにフィールドを追加する (Prismaで自動追加)
  4. :writing_hand: リソルバーにフィールドを追加する (Nexusにフィールド追加)
  5. :writing_hand: ORM経由で追加したフィールドも取得するようにする
  6. :writing_hand: フロントエンドのクエリにフィールドを追加する
  7. :robot: クエリの返り値の型を変更する (graphql-code-generatorが自動で型生成)

もともと6つだった作業が4つに減りました (Prismaのスキーマファイル記述タスクが増えています)。
手動でやるべきタスクはまだ残っていますが、データベースにカラムを追加したり、クエリの返り値の型を変更したり
といった煩わしい作業は減っているので効率的になっているのはないでしょうか。

補足: nexus-prismaでリゾルバーの実装を楽にする

nexus-prismaというnexusのプラグインを使うことによって、Prismaのデータモデルのフィールドをリゾルバを密接に結びつけることができ、
オブジェクトの定義、QueryやMutationの実装を極力抑えることができます。
詳しくはnexus-prismaのドキュメントを見ていただいたほうが早いと思うのですが、
例えば、ユーザーのデータモデルが以下のように定義されているとします。

prisma.schema
model User {
  id        String   @id @unique @default(cuid())
  email     String   @unique
  birthDate DateTime
}

ユーザーをIDで取得するクエリを追加したいとしましょう。
通常であれば以下のように、Photon.jsを呼びデータを取得するロジックを書く必要があります。

export const Query = queryType({
  definition(t) {
    t.field("user", {
     type: "User",
     resolve(root, args, ctx) {
       return ctx.photon.users.findOne({ where: { id: args.id }})
     }
    })
  },
})

prisma-nexusを使えば一行でクエリを実装することができます。

schema.ts
export const Query = queryType({
  definition(t) {
    t.crud.user()
  },
})

この場合Prismaのスキーマファイルでuniqueのアノテーションが付けられたフィールドで検索ができるようになります。
なので、クエリを投げる際は以下のようにします。

query findUser {
  user(where: { email: "hogehoge@hoge.com" }) {
    id
  }
}

とても便利なのですが、Prismaと密結合してしまっているので、将来的にPrismaを使わない判断をしたときに、リファクタリングがしづらいことを考えて私は使っていません。
クリーンアーキテクチャでいうところの内側のレイヤーが外側のレイヤーに依存してしまっている形になっているので。

まとめ

TypesSript + GraphQLによるエンドツーエンドな開発をできるだけスムーズにするための方法について簡単ではありますが説明しました!
今回はNexusやPhotonなど特定のツールを使いましたがTypedGraphQLやTypeORMなど状況に応じてライブラリを選択していただき、
より快適なTypeScript + GraphQLライフにしていただければと思っています!
来年もGraphQL界隈がもっと盛り上がることを期待していますし、自分も少しでも貢献できればいいと思っています。

  1. レポジトリ自体はprisma-labにありますが、Prismaが開発しているわけではないようです。
    GraphQLには大きく分けてスキーマファーストかコードファーストな開発があると思いますが、Nexusはコードファーストのアプローチを採用しています。
    2つのアプローチには一長一短あると思いますが、Nexusがコードファーストなアプローチをとっている理由はprismaのブログに書かれています。
    他のライブラリとして以下があります。

21
15
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
21
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?