🧠

GraphQL Server実装におけるSchema FirstとCode Firstの比較

2023/10/22に公開

はじめに

株式会社CHILLNNという京都のスタートアップでCTOを務めております永田と申します。

弊社では、現在、4年程度運用してきたサービスの技術的負債の返済を進めており、TypeScriptでのGraphQLサーバー実装の技術選定を行ったため備忘録がてら記事にしておきます。

この記事の目的

GraphQLサーバーを構築する際には主に以下の二つの実装を行う必要があります。

  1. 全ての型とフィールドを定義するSchema
  2. Schemaの型データを返すために呼び出される関数のコレクションであるResolver

本記事では、SchemaをSingle Source of Truth(SSOT)とするSchema Firstでの開発アプローチと、サーバー実装をSSOTとするCode Firstでの開発アプローチについて長所・短所を比較し、最終的に我々がどのような意思決定をしたのかをご紹介します。

Schema First

Schema Firstとは、最初にGraphQLのSchemaを定義し、次にSchemaの定義に一致するコードを実装する実装手法を指します。Schemaを定義するためには、GraphQLデータモデルを表現するためのSDL(Schema Definition Language)を利用します。そのため、このアプローチはSDLファーストとも呼ばれます。

SDLの可読性は非常に高く、特定の言語に縛られない柔軟な仕様となっています。
GraphQLの最初の実装とUtilityの提供はJavaScriptでしたが(2015年にgraphql-js、2016年にgraphql-tools)その後様々な言語でSDLを解決できるサーバーが実装されました。
(ちなみにGraphQLのSDLは、GraphQLよりも後に登場しました。)

SDLは以下のようなものになっています。

type Hotel {
    id: ID!
    name: String!
    rooms: [Room!]
}

上記は、idnameそしてroomをプロパティとして持つ、Hotelの型です。
nameが文字列型の必須のプロパティであり、roomというオブジェクトを複数持つことが簡潔に定義できます。

ちなみに、Roomの型は以下のように表現できます。

type Room {
    id: ID!
    name: String!
}

SDLは非常にシンプルで簡潔であるため、どのような言語を扱う技術者や非技術者でも理解しやすく、チームの枠を超えて、組織のデータモデルを定義するためのドキュメントとして機能します。

一方、欠点として、SDLにはfieldの値を計算して返すResolverの実装が含まれていません。
GraphQLを機能させるためには、何らかの言語でResolverの実装を追加で行わなくてはならず、GraphQLのみをSingle Source of Truthとすることはできません。

Code First

Code Fistは、開発者はResolverだけを書くのみでよく、ビルドツールがコードに含まれる型情報やアノテーションに基づいてSDLをコンパイルするというアプローチのことです。
Code Onlyや、Resolver Firstと呼ばれることもあります。

特定の言語から、別の言語を生成するのは、メタ情報の付与など、ある程度複雑な処理を要するため、Code Firstでの実装はSDLに比べ可読性が下がります。

以下の例は、Nexus(JavaScript/TypeScriptによるCode FirstのGraphQLサーバー)で、Schema Firstのセクションで例示したSDLと同じものを実装したものです。

const Hotel = objectType({
  name: "Hotel",
  definition(t) {
    t.id("id");
    t.string("name");
    t.field("rooms", {
      type: list(nonNull(Room)),
      resolve(root, args, ctx) {
        return ctx.getHotel(root.id).rooms();
      },
    });
  }
});

const Room = objectType({
  name: "Room",
  definition(t) {
    t.id("id");
    t.string("name");
  }
});

Nexusは宣言的で読みやすいことを重視しています。
SDLよりは理解しづらいとはいえ、Schema定義に加えSchemaがどのように解決されるかまで表現しています。

Code Firstは、Schema定義とコード実装の両方が含まれているが故に、データモデル上のSingle Source of Truthとなりえます。
その代償として複雑さが増し可読性が下がります。

それぞれの長所の詳細

それぞれのアプローチについて、一般的に言われている長所について紹介していきます。

Schema Firstが優れている理由

Schema Firstが重視するのは「保守性の高いコード」です。

理由1: バックエンドの技術的詳細から切り離して考えることができる

Schema Firstを選択する上での利点の一つは、開発プロセスにおけるAPIのインターフェースの設計において、その他の実装を切り離して考えることができる点にあります。

バックエンドの開発者がAPIを設計する際、普段触っているバックエンドのコードの先入観を完全に切り離して考えることは難しくほとんど不可能に近いです。

Schema Firstでのアプローチでは、そもそも言語が異なっているため、バックエンドのコードの詳細から切り離して考えることを強制することができます。

実装のフローにおいては、Schema Firstでの開発はTDD(テスト駆動開発)に近いと考えることができます。
TDDでは、ソリューションを実装する前に、ソフトウェアをクライアントとして使用します。
この順序で実装を行うことで、開発者はまずユースケースや開発者にとっての利便性を考慮せざるをえなくなります。これは、技術的詳細に踏み込む前に、まず使いやすいインターフェースについて考える必要があるということです。
結果的に、コードはハイレベルな抽象に依存することになり、アプリケーションはモジュール化された保守しやすい設計になります。

Schema Firstでの実装は、ソフトウェアの技術的詳細について考えるよりも前にユースケースについて考えます。
これはTDDでの開発アプローチと同様に、API設計においてバックエンドの実装よりも先に呼び出しのインターフェースを考えることになります。結果的に、API呼び出しとバックエンドの実装詳細の関係性が、依存性逆転の原則に則ったものとなり、保守しやすい設計となります。

理由2: API定義に多くのステークホルダーが関わりやすくなる

APIが異なる責務を担うソフトウェアの中間に属しているが故に、新規にAPIを実装する際には、バックエンドの開発者だけでなく、フロントエンドの開発者、できればドメインエキスパートも同じレベルで議論に参加できることが理想です。
SDLの簡潔な記述は、上記のような議論を行う際の唯一の成果物として利用することができます。

Code Firstが優れている理由

Code Firstが重視するのは「開発体験の良さ」です。

理由1: 一つの道具で実装できる、DRYになる

Schema FirstでのアプローチはどうしてもDRYではない、冗長なコードになってしまいます。
これは、依存性逆転の原則に従った場合には避けられないトレードオフです。

SDLで定義したSchemaとResolverのマッピングを担当する抽象化レイヤーを管理し、常に同期をさせる必要が生じます。このレイヤーを管理するために、コミュニティから便利なツール群が提供されていますが、そのツール群を扱うために、新たな学習コストが発生し、開発プロセスやリリースフローが複雑化してしまいがちです。

一方、Code Firstでのアプローチでは、Schemaがコードから生成されるため、常に同期されていることが保証されており、この複雑さが発生しません。複雑なツール群に依存せずに、シンプルな開発モデルを実現することで、開発時の複雑さの増大を回避することができます。

理由2: 言語やIDEのサポートを受けた素早い実装ができる

Schema Firstでのアプローチは、SDLを独立して定義しているため、プログラミング言語ごとに持っている機能のサポートを受けることができません。

Code Firstでのアプローチでは、言語ごとに持っている型システムのサポートを受けることによって、自動補完や、ビルド時のエラーチェックなどの機能を活用することができます。
開発の全部において、言語ごとのエコシステムの資産を活かした開発を行うことができます。

最終的にどのように判断したか

結論から言うと、弊社では最初はCode First(Pothos)で進めましたが、途中で考えを改め、Schema Firstでのアプローチに変更しました。

理由1: データモデルとドメインモデルに差分が生じていた

開発を続ければ、常に新たな知識が発見され、ドメインモデルは更新され続けていきます。
開発の初期段階においては、ドメインモデルとデータモデルは完全に一致しているはずで、弊社では初期開発時点でGraphQL Schemaから生成した型定義ファイルをほとんどEntityとして利用してきました。

しかしアジャイルに開発を進めていく中で、二つのモデル間に徐々に差分が生じ、バックエンドに何層か差分を吸収するためのレイヤーを導入する必要がありました。
当然、クライアントからの呼び出し部分でも、新規に型定義を行う必要がありました。

結果的に、どちらのアプローチを選ぼうが依存性逆転の原則に則った場合のトレードオフである冗長性は避けられず、Code Firstの恩恵を受けることは難しいと判断し、Schema Firstの可読性を優先しました。

理由2: Code FirstでのSchema定義を冗長に感じた

技術選定にあたって、検討したフレームワークは、TypeGraphQL, Nexus, Pothosの3つです。

どれも型システムの恩恵を最大限受けることができ自動補完が効くのですが、TypeScriptの
言語仕様上、実行時には型システムの恩恵は受けられないため、記述が冗長(体感GraphQLのSDLを書く10倍くらい)に感じられ、その複雑性からコンパイルされるSchemaのイメージがしづらく、これはほとんどフレームワーク依存のDSL(ドメイン固有言語)なのでは、、、?と思ってしまいました。学習コストの高さに比べて、知識の汎用性の低さを実感し、フロントエンドチームにこの知識を要求することはできないと判断しました。
ちなみにTypeScriptではなく、他の静的型付け言語であれば、もっと簡潔に書ける言語はあるはずです。

結果的に、Code Firstでのアプローチを選んだとしても、フロントエンドチームとAPIの仕様について議論する際には、GraphQLのSDLを用いるんだろうなと想像ができたので、だとしたら、最初からSchema Firstで実装した方が早くない?と結論づけました。

理由3: 開発コミュニティに不安があった

理由2で挙げた3つのフレームワークなのですが、こちらの記事でも言及されていますが、現時点では健全とは言えない開発状況だと感じました。

TypeGraphQL
Nexus
Pothos

ちなみに、現時点でのnpm trendはこんな感じになっています。
@pothos/core vs nexus vs type-graphql

最近勢いのあったPothosのDL数が急激に落ち込んでいます。

ついでに、TypeScriptでのGraphQLサーバーに用いられるパッケージの情報も置いておきます

Apollo Server
GraphQL Yoga

まとめると、TypeScriptのCode Firstのフレームワークでは以下のような問題が生じています。

  • 開発が活発でない
  • 少人数の開発者のみに依存している

実際にフレームワークを使ってみればわかるのですが、TypeScriptの言語仕様上Code Firstのフレームワークを簡潔に作ることはかなり難しく、必ずどこかで冗長性が生じます。どんな冗長性、トレードオフを許容するかは、完全に好みが分かれるので、開発者が分散してしまいどのフレームワークでも将来性に対しての不安は拭えないと感じました。

ちなみに、初期開発であれば、Code Firstを採用することで開発速度を上げることが期待できるとは思います。しかし、かなりフレームワークへの依存性が高いコードにはなってしまうと思うので、その辺はトレードオフとして認識しておくべきかと思います。

理由4: Schema Firstでの開発に必要なツール群がとても充実している

Schema Firstで実装するにあたって、SchemaとResolverのマッピングを担うコードは、GraphQL Code Generatorなどを使うことで自動生成が可能です。

Schemaファイルの分割も容易にできますし、Code Firstで実装した場合には手動での管理が必要なContextの型情報のDIも自動で行ってくれます。本当に、神ツールすぎて開発コミュニティに頭が上がりません、、、。

まとめ

自分の中では、TypeScriptでGraphQLサーバーを実装する場合は、Schema Firstを選択しておけば間違いないと思っています。

一方、開発の初期段階などで、データモデルとドメインモデルが一致しておりバックエンドでレイヤーを分ける必要がないようなアプリケーションであれば、Code Firstのフレームワークを選択することで、素早くDRYな実装を行えることも期待できます。

単に技術観点だけではなく、ビジネス観点からも技術選定を捉え、ある程度将来の技術的負債を許容して、開発速度を優先させる意思決定を行う必要がある場合も考えられるので、それぞれのプロジェクトを長期的な時間軸で捉えた上で意思決定をすることが必要です。

参考記事

執筆にあたって大変参考にさせていただいた記事をご紹介します。

最後に

株式会社CHILLNNではエンジニアを積極的に採用しています!
現在、関西圏のSIerなどにお勤めの方で、モダンな技術スタックで、納期に縛られずクオリティにこだわって自社プロダクトを作っていきたいエンジニアの方がいましたら、是非是非ご連絡ください!
まだ小さい会社ですが、必ず大きくなります。ぜひ一度お話しさせてください〜〜
https://chillnn-inc.studio.site/
https://www.wantedly.com/projects/1355193

CHILLNN Tech Blog

Discussion