🪴

Pothosで始めるコードファーストなGraphQL開発

に公開

Pothosは型安全性を実現するためのコードファーストなGraphQLスキーマビルダーで、TypeScriptで記述したコードから簡単にGraphQLスキーマを構築できるライブラリです。

https://pothos-graphql.dev/

本稿ではPothosの特徴と弊社でのどのように使用しているかについて紹介します。

Pothosの特徴

コードファーストによる型安全性

Pothosはコードファーストのアプローチを採用しており、オブジェクト型・クエリ・ミューテーションなどGraphQLの要素全てをTypeScriptで表現します。

TypeScriptのコードを元にスキーマが生成されるため、スキーマに合わせた型定義を再度TypeScriptで実装する工程を省略しつつ、TypeScriptの補完・静的解析の恩恵を存分に受けることができます。

以下はPothos の SchemaBuilderUser 型と user クエリ、createUser ミューテーションを定義している実装例です。

builder.ts
import SchemaBuilder from "@pothos/core";

const builder = new SchemaBuilder({});

type User = {
  id: string;
  name: string;
}

const UserRef = builder.objectRef<User>("User");

UserRef.implement({
  description: "ユーザー",
  fields: (t) => ({
    id: t.exposeString("id"),
    name: t.exposeString("name"),
  }),
});

builder.queryType({
  fields: (t) => ({
    user: t.field({
      type: UserRef,
      description: "ユーザーを取得する",
      resolve: () => ({
        id: "1",
        name: "John Doe",
      }),
    }),
  }),
});

builder.mutationType({
  fields: (t) => ({
    createUser: t.field({
      type: UserRef,
      args: {
        name: t.arg.string({ required: true }),
      },
      description: "ユーザーを作成する",
      resolve: (root, args) => {
        return { id: "1", name: args.name };
      },
    }),
  }),
});

上の実装から以下のようなGraphQLスキーマが生成されます。TypeScriptで書かれた情報からのみスキーマが生成されていることがわかります。

schema.graphql
type Mutation {
  """ユーザーを作成する"""
  createUser(name: String!): User
}

type Query {
  """ユーザーを取得する"""
  user: User
}

"""ユーザー"""
type User {
  id: String
  name: String
}

スキーマのメンテナンスコスト削減

スキーマがTypeScriptコードから自動生成されるため、手動でスキーマファイルを管理する必要がなく、実装とスキーマが乖離する問題を防げます。

また、実際に実装されたリゾルバだけがスキーマに反映されるため、「スキーマだけ先に定義したがスキーマの通りに実装できなかった」という手戻りも発生し得ません。

プラグインエコシステムによる拡張性

Pothosはプラグインベースで設計されており、GraphQLサーバー構築に必要な多くのプラグインを公式で提供しています。

プラグインの導入は、該当パッケージをインストールし、SchemaBuilderpluginsオプションに指定するだけです。

add-plugin.ts
import SchemaBuilder from '@pothos/core';
import AddGraphQLPlugin from '@pothos/plugin-add-graphql';

const builder = new SchemaBuilder({
  plugins: [AddGraphQLPlugin],
});

DataloaderやPrisma/Drizzleとの連携、Relay仕様のカーソルベースページネーションなど、多様なプラグインが提供されており、必要なものだけを選んで無理なく取り入れることができます。

https://pothos-graphql.dev/docs/plugins

弊社での運用

DDDベースのアーキテクチャに載せる

弊社では以下のような形式でAPIを定義しています。

src
├─ 🪴core
│  └─ builder.ts
├─ 🪴resolvers
│  └─ users
│     ├─ fields
│     ├─ mutations
│     ├─ queries
│     └─ types
├─ usecases
│  └─ users
│     ├─ fields
│     ├─ mutations
│     ├─ queries
│     └─ types
└─ index.ts

Pothosを利用するのはcoreとresolversのみで、usecases以下はGraphQLを意識せずビジネスロジックをTypeScriptで実装します。

弊社が公開しているOSSがこの構成に準じているため参考にしてみてください(以降で添付しているサンプルコードもこのリポジトリのものです)。

https://github.com/praha-inc/feedbackun/tree/main/apps/web/src/graphql

builder.ts

PothosのSchemaBuilderを初期化してエクスポートするファイルです。

よく使う設定やプラグインを適用済みのSchemaBuilderを提供し、プロジェクト内では全てこのSchemaBuilderを通してオブジェクト型やクエリを定義します。

https://github.com/praha-inc/feedbackun/blob/main/apps/web/src/graphql/core/builder.ts

resolvers

エクスポートされたSchemaBuilderを使用してGraphQLのリゾルバを定義するディレクトリです。

ドメインごとにディレクトリを分割し、さらにそのディレクトリをfieldsmutationsqueriestypesのように用途ごとに分割します。

resolversの役割はusecasesディレクトリに定義された対応する単一の関数を呼び出すことにとどめており、ビジネスロジックは一切記述しません。

https://github.com/praha-inc/feedbackun/tree/main/apps/web/src/graphql/resolvers/feedbacks

usecases

ユースケースを実現するロジックを記述するディレクトリです。

GraphQLの存在は意識せずユースケースを実現するための関数だけを記述します。

usecases以下はPothosが登場しないため割愛します。

https://github.com/praha-inc/feedbackun/tree/main/apps/web/src/graphql/usecases

index.ts

アプリケーションのエントリポイントとして API を公開するファイルです。

GraphQLサーバーの部分は特定の技術に限定されませんが、弊社ではYogaを使用することが多いです。

https://github.com/praha-inc/feedbackun/blob/main/apps/web/src/graphql/index.ts

スキーマファイルをコミットし、スキーマ変更をレビュー可能にする

コードファーストには、リゾルバの差分だけではスキーマ全体の構造変化が直感的に追いづらいという欠点があります。

そこで弊社ではビルド時にschema.graphqlを自動生成し、それをリポジトリにコミットする運用を採用しています。

具体的にはTurborepoのタスク間の依存関係解決によりビルドのたびにprintschemaを実行されるようにすることで、開発中の動作確認やビルド時にリゾルバの変更が常にスキーマに反映されるようにしています。

https://github.com/praha-inc/feedbackun/blob/main/apps/web/scripts/write-schema.ts

この運用により、PR上でスキーマ差分とリゾルバ差分をあわせてレビューでき、変更されたフィールドが一目で把握できるようになっています。

リゾルバから実装するかユースケースから実装するかは柔軟に選択する

「スキーマを先に定義すれば、バックエンドとフロントエンドを並行して開発できる」という点はスキーマファーストの利点として語られますが、弊社ではコードファーストでもこのメリットを享受できています。

具体的には次のような仮実装リゾルバだけを一旦作成し、そのリゾルバとリゾルバから生成されたスキーマをレビューします。Pothosは型情報からGraphQLスキーマを生成するためリゾルバの実装自体は先延ばしにすることができます。

import { builder } from '../../../core/builder';

const CreateUserInput = builder.inputType('CreateUserInput', {
  description: 'ユーザー作成の入力',
  fields: (t) => ({
    name: t.string({ description: '名前' }),
  }),
});

builder.mutationField('createUser', (t) => t.field({
  type: User,
  description: 'ユーザーを作成する',
  args: {
    input: t.arg({ type: CreateUserInput }),
  },
  resolve: () => {
    // TODO: usecasesを呼び出すように書き換える
    return {
      user: {
        id: '1',
        name: 'John Doe',
      },
    };
  },
}));

GraphQLの理解が浅いメンバーがいる場合や、Input/Outputを慎重に検討したい場合、先にスキーマだけ作ってフロントと並行開発したい場合に有効な手法です。

まとめ

筆者の観測範囲ではまだスキーマファーストが主流のように感じますが、Pothosであれば型安全性・拡張性・レビューしやすさを兼ね備えた快適なGraphQL開発が可能です。TypeScript × GraphQLでサーバーを構築している方は、選択肢として検討する価値があると思います。

\ PR /

弊社は中級エンジニアを育てるオンライン・プログラミング・ブートキャンプ「プラハチャレンジ」を運営しています。

ちょうど第11期の募集が始まっておりますので、興味がある方いらっしゃいましたら是非に!

https://praha-challenge.com/

もちろんダイレクト応募もお待ちしております。

https://entrance.praha-inc.com/web-engineer-recruit

PrAha

Discussion