💡

【GraphQL in Nextjs】nexusでGraphQLスキーマ設計を効率的に行おう!

2022/06/06に公開

こんにちは、@nerusanです。
今回は、GraphQLのスキーマ設計でnexusが便利だったので紹介しようと思います。

https://nexusjs.org/

通常、GraphQLを利用する時は、スキーマ定義言語(Schma Definition Language)を使用してスキーマを定義し、データを提供するためのリゾルバーロジックを記述します。

例えば、タスク一覧のデータを返すAPIを考えると、以下のようになります。

schema.ts
import { gql } from 'apollo-server-micro'
// スキーマ定義
// タスクを扱う型
export const typeDefs = gql`
  type Task {
    id: Int
    title: String
    done: Boolean
  }

  type Query {
    tasks: [Task]!
  }
`
resolver.ts
// リゾルバーの記述
// 一旦JSONの固定値を返すロジックにしている
export const resolvers = {
  Query: {
    tasks: () => {
      return [
        {
          id: 1,
          title: 'task 1',
          done: false,
        },
        {
          id: 2,
          title: 'task 2',
          done: false,
        },
      ]
    }
  }
}
index.ts
import { ApolloServer } from 'apollo-server-micro'

// apolloserverにスキーマーとリゾルバーのセット
const server = new ApolloServer({
  typeDefs,
  resolvers,
  csrfPrevention: true,
});

// サーバーをリッスン
server.listen().then(({ url }) => {
  console.log(`🚀  Server ready at ${url}`);
});

スキーマから定義し、リゾルバーを定義するため、
このアプローチはしばしばスキーマファーストと呼ばれます。
スキーマファーストのアプローチには以下の欠点があります。

  • スキーマ設計とリゾルバー設計が異なるので、作業が面倒くさい上、プロジェクトが大きくなるに連れて、わかりにくい
  • スキーマ言語とコード(JavaScript/TypeScript)で同じような記述が繰り返し書くため、typoなどのエラーが起きややすいし、気づかない
  • スキーマ言語とJavaScript/TypeScriptの言語的記述方法が異なるので、精神的に苦痛
  • エディタ補完が利用できない

そこで、nexusを利用することでそれらの問題を解決することができます。コードベース(TypeScript/JavaScript)で定義できるので、コードファーストと呼ばれることがあります。

以下のメリットがあります。

  • GraphQLのSDL(Schema Definition Language)ではなく、コードファースト(JavaScript/TypeScript)でスキーマを定義することができる
  • SDLではschemaとresolverの定義が分離していたが、Nexusでは2つを同じファイルで定義することができる
  • SDLファイルと型定義を自動生成してくれる
  • エディタで補完が効く
  • 型検知により、Typoは、エラーになるため、すぐ気づく

詳しくは公式ページをご覧ください。

https://nexusjs.org/docs/getting-started/why-nexus

それでは、実際に使ってみましょう!

nexusを使ってみよう

まずは、インストールします。

$ yarn add nexus

スキーマとリゾルバーを定義していきます。

types/Task.ts
import {
  objectType,
  extendType,
  nonNull,
  stringArg,
  intArg,
  booleanArg,
} from "nexus";

// 型定義
// タスクの型
export const Task = objectType({
  name: "Task",
  description: "タスク一覧の型定義",  // 各種パラメータの説明をdescriptionで記述できる
  definition(t) {
    t.nonNull.int("id", { description: "タスクのid" });
    t.nonNull.string("title", { description: "タスクのタイトル" });
    t.nonNull.boolean("done", { description: "完了フラグ" });
  },
});

// 返すデータを設定
export const TasksQuery = extendType({
  type: "Query",
  definition(t) {
    t.nonNull.list.field("tasks", {
      description: "タスク一覧配列を返す",
      type: "Task", // 返り値の型タイプ
      resolve(_parent, _args, ctx) {
        return [
          {
            id: 1,
            title: 'task 1',
            done: false,
          },
          {
            id: 2,
            title: 'task 2',
            done: false,
          },
        ]
      },
    });
  },
});

全てエクスポートします。

types/index.ts
export * from './Task'

設定などを記述します。

schema.ts
import { makeSchema } from 'nexus'
import { join } from 'path'
import * as types from './types/index'

export const schema = makeSchema({
  types,  // スキーマとリゾルバーの設定
  outputs: {
    // 型定義ファイルをnode_modules/@types/nexus-typegen/index.d.tsに生成する設定
    typegen: join(process.cwd(), 'node_modules', '@types', 'nexus-typegen', 'index.d.ts'),
    // GraphQL SDLファイルをgraphql/schema.graphqlに生成する設定
    schema: join(process.cwd(), 'graphql', 'schema.graphql'),
  }
})

エンドポイントをapollo serverで設定

index.ts
import { ApolloServer } from 'apollo-server-micro'
import { schema } from './schema';

// apolloserverにスキーマーとリゾルバーのセット
const server = new ApolloServer({
  schema,
  csrfPrevention: true,
});

// サーバーをリッスン
server.listen().then(({ url }) => {
  console.log(`🚀  Server ready at ${url}`);
});

型定義とリゾルバーの記述はtypes/Task.tsに書かれており、すっきりとしていることがわかるのではないでしょうか。実装もとても楽になりそうです。

説明もdescriptionで指定することができます。descriptionに書かれた説明は、apollo studioに表示されます!その説明を見て、クライアント側も実装できるので、ドキュメントがわりにもなりますね。

また、TypeScriptを利用し、コード補完が効くため、コードも素早く書けます。また、タイポもすぐ教えてくれます。例えば以下のようなミスもすぐにエラーが表示されます。

types/Task.ts
      resolve(_parent, _args, ctx) {
        return [
          {
            ip: 1,   // エラーです
            title: 'task 1',
            done: false,
          },
          {
            id: 2,
            title: 'task 2',
            done: false,
          },
        ]

便利ですね!

また、outputに記述したファイルが作成されるタイミングですが、
ブラウザでAPIのエンドポイントにアクセスすると、apollo sutudioにアクセスするリンクが表示される(https://studio.apollographql.com/sandbox/explorer)ので、そこ にアクセスした状態で、コードを保存すると自動的に、schema.graphqlと型定義が作成されます。(なぜ、これでできるのかまだ、理解できていません。。)

prismaとの利用

また、prismaとの相性もいいです。リゾルバーとして、prismaを利用して、DBの値を返すこともできます。
以下のように設定します。

prisma.ts
import { PrismaClient } from "@prisma/client";

declare global {
  var prisma: PrismaClient | undefined;
}

// prisma clientを返す
export const prisma =
  global.prisma ||
  new PrismaClient({
    log: ["query"],
  });

if (process.env.NODE_ENV !== "production") global.prisma = prisma;
context.ts
import { PrismaClient } from "@prisma/client";
import { prisma } from "./prisma";

export type Context = {
  prisma: PrismaClient;
};

/**
 * resolverがPrismaClientにアクセスし、データベースの値を変換できるようにするため、prisma clientを返す
 * @returns prismaClient
 */
export async function createContext(): Promise<Context> {
  return {
    prisma,
  };
}
schema.ts
import { makeSchema } from 'nexus'
import { join } from 'path'
import * as types from './types/index'

export const schema = makeSchema({
  types,  // スキーマとリゾルバーの設定
  outputs: {
    // 型定義ファイルをnode_modules/@types/nexus-typegen/index.d.tsに生成する設定
    typegen: join(process.cwd(), 'node_modules', '@types', 'nexus-typegen', 'index.d.ts'),
    // GraphQL SDLファイルをgraphql/schema.graphqlに生成する設定
    schema: join(process.cwd(), 'graphql', 'schema.graphql'),
  },
+ contextType: {
+   // context.tsファイルを指定
+   export: "Context",
+   module: join(process.cwd(), "context.ts"),
+ },
})

そうするとリゾルバーで以下のようにアクセスできる。

types/Task.ts
export const TasksQuery = extendType({
  type: "Query",
  definition(t) {
    t.nonNull.list.field("tasks", {
      description: "タスク一覧配列を返す",
      type: "Task", // 帰り値の型タイプ
      resolve(_parent, _args, ctx) {
+       return ctx.prisma.task.findMany(); // データベースの値を返す
      },
    });
  },
});

詳しくは公式のサンプルコードをご覧いただくといいです。

https://github.com/graphql-nexus/nexus/tree/main/examples/with-prisma

まとめ

どうでしたでしょうか。コードファーストにかけ、TypeScriptの恩恵も受けれることができるので、とても楽になります。

GraphQLを利用する際は是非積極的に利用を検討したいツールですね。

参考

https://zenn.dev/youichiro/articles/9e028d0a3b45e3

GitHubで編集を提案

Discussion