Next.js + GraphQL Nexus で作る typesafe な GraphQL フルスタックアプリケーションの手引き

2020/12/26に公開

自分のブログにも載せています。興味ある方はこちらまで!

どうも、@yuyaaar です。

先日書いた記事 - Next.js + Prisma + NextAuth.js + React Query で作るフルスタックアプリケーションの新時代がありがたいことにとても反響があり、嬉しい限りです。皆さんありがとうございます。

いやーー、ありがたい、、

今回は、その書いた記事の外伝版を書きたいと思います。

何がどう外伝なのか?

昨日書いたチュートリアルは、クラシックな REST API での実装方法でした。

今回は、それに変わって GraphQL での実装方法を書いていきたいと思います。

GraphQL の実装は少し手間がかかるので、この記事ではデプロイや認証機能の説明は割愛させていただきます。

Next.js の ts ボイラープレートアプリやデータベースの作成方法、Prismaのインストールはこのチュートリアルを参考にしてください。

データベースにこのようなスキーマを作り、

model User {
  id   Int @id @default(autoincrement())
  name String
}

postgreSQL データベースに接続、マイグレーションを走らせてから初めてください。

Step 1 (インストール)

まずは必要な API Route を GraphQL エンドポイントにするために必要なライブラリをインストールしましょう。

$ yarn add apollo-server-micro nexus nexus-plugin-prisma
  • apollo-server-micro - サーバーレス環境に適した Apollo GraphQL サーバー
  • nexus - Code first GraphQL schema
  • nexus-plugin-prisma - Prisma の型を nexus に自動的に引っ張ってくるためのミドルウェア

Step 2 (エンドポイントの作成)

pages/api/graphql.ts というファイルを作りましょう。全ての GraphQL リクエストはここで処理されます。

そして中身をこのように

import { ApolloServer } from "apollo-server-micro";

// これはあとで作ります
import { schema } from "../../graphql/schema";
import { createContext } from "./../../graphql/context";

const apolloServer = new ApolloServer({
  context: createContext,
  schema,
  tracing: process.env.NODE_ENV === "development",
});

export const config = {
  api: {
    bodyParser: false,
  },
};

export default apolloServer.createHandler({
  path: "/api/graphql",
});

このファイルは Apollo サーバーを handler として export しているだけですね。

そして、ルートディレクトリに graphql フォルダを作り、graphql/schema.tsgraphql/context.ts を作ります。

// graphql/schema.ts

import { queryType, makeSchema } from "nexus";
import path from "path";

const Query = queryType({
  definition(t) {
    t.string("hello", { resolve: () => "hello world" });
  },
});

export const schema = makeSchema({
  types: [Query],
  outputs: {
    typegen: path.join(process.cwd(), "generated", "nexus-typegen.ts"),
    schema: path.join(process.cwd(), "generated", "schema.graphql"),
  },
});

このファイルでは GraphQL Nexus を使用しています。

普通だと、スキーマを SDL で書いて、それに合わせた resolver を書く、みたいなのが主流だと思います。

ですが、nexus では JavaScript/TypeScript でスキーマと resolver をコロケーションすることがき、スキーマに合わせて GraphQL ファイルを自動生成してくれます(typegen で指定したところに)。

その他諸々利点はあるのですが、詳しくはこちらで!

// graphql/context.ts

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

const prisma = new PrismaClient();

export type Context = {
  prisma: PrismaClient;
};

export const createContext = (): Context => ({
  prisma,
});

最後に、 nexus で使用する context に prisma の型情報を入れましょう。

これで一度 yarn dev で dev サーバーを立ててみてください。

localhost:3000/api/graphql で GraphQL Playground が立ち上がれば、成功です。

Step 3 (prisma との連携)

以下に変更してください。

// graphql/schema.ts

import { queryType, makeSchema, objectType } from "nexus";
import { nexusPrisma } from "nexus-plugin-prisma";
import path from "path";

const Query = queryType({
  definition(t) {
    t.string("hello", { resolve: () => "hello world" });
  },
});

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

export const schema = makeSchema({
  types: [Query, User],
  plugins: [nexusPrisma({ experimentalCRUD: true })],
  outputs: {
    typegen: path.join(process.cwd(), "generated", "nexus-typegen.ts"),
    schema: path.join(process.cwd(), "generated", "schema.graphql"),
  },
  contextType: {
    module: path.join(process.cwd(), "graphql", "context.ts"),
    export: "Context",
  },
  sourceTypes: {
    modules: [
      {
        module: "@prisma/client",
        alias: "prisma",
      },
    ],
  },
});

何をしたのかというと、 GraphQL サーバーに prisma で作成した User オブジェクトを API レイヤーに写しました。

ここで API に表面化する値は、自分で決めれます。

例えば、 prisma のスキーマに password のフィールドがあり、それは API レイヤーに露出したくない場合は、t.model として書かなければ露出されません。

一度、 t.model... のところをいじってみてください。 TypeScript が prisma のスキーマに沿って、 auto-complete してくれるのがわかると思います。

もう一度サーバーを立てて /api/grahpql にアクセスすると、 User モデルが Schema タブの中にあるはずです。

では、簡単な Query も書いてみましょう。

Query をこのように変えてみてください。

const Query = queryType({
  definition(t) {
    t.crud.users();
    t.crud.user();
  },
});

これだけで、ユーザーを探す・多数のユーザーからの絞り込みができるクエリが完成しました。

セーブして自動生成されたコード(generated/schema.graphql)を見ると分かりますね。

### generated/schema.ts

type Query {
  users(
    first: Int
    last: Int
    before: UserWhereUniqueInput
    after: UserWhereUniqueInput
  ): [User!]!

  user(where: UserWhereUniqueInput!): User
}

input UserWhereUniqueInput {
  id: Int
}

crud だけではなく、カスタマイズもできます。例えば、

// graphql/schema.ts

const Query = queryType({
  definition(t) {
    t.list.field("getAllUsers", {
      type: "User",
      resolve(_, _args, ctx) {
        return ctx.prisma.user.findMany({});
      },
    });
    t.crud.users();
    t.crud.user();
  },
});

こんな感じ。

  • t.list とすることによって、このクエリは配列を返すと指定しています。
  • type は GraphQL の scalar type もしくは自分で作った objectType。ここで指定した type を resolve では返さないといけません。
  • resolve の中の ctx は先ほど作成した graphql/context.ts の情報が含まれているので、 prisma の型情報の恩恵を受けれます。

次は mutationType も作ってみましょう。

// graphql/schema.ts

const Query = queryType({ ... })

const Mutation = mutationType({
  definition(t) {
    t.crud.createOneUser()
  }
})

mutationType の中でも t.crud とすることができて、変更を伴うシンプルな CRUD は自動で生成してくれます。

mutationType も queryType と同じくカスタマイズ可能で、こんな感じで書けます。

const Mutation = mutationType({
  definition(t) {
    t.crud.createOneUser();
    t.field("deleteAllUsers", {
      type: "String",
      async resolve(_parent, _args, ctx) {
        const { count } = await ctx.prisma.user.deleteMany({});
        return `${count} users deleted.`;
      },
    });
  },
});

作成した mutationType は makeSchema 内の types 配列に入れ忘れずに。入れないと反映されません。

Step 4 (フロントエンドと型の自動生成)

サーバー側ではちゃんと型があるのに、フロントでないと意味ないですよね。

でも、型が二重管理になってしまいメンテできなくなったりすることが多いと思います。

そこで、バックエンドの型情報をフロントに持ってきてくれて、かつ自動生成してくれる優れものが graphql-codegen です。

今回も、前の記事と同じように react-query をフロントのクエリライブラリとして使っていきます。

まず graphql-codegen の CLI やらを一通りインストール

$ yarn add react-query graphql-tag graphql
$ yarn add @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-graphql-request @graphql-codegen/typescript-operations --dev

そして、ルートディレクトリに codegen.yml ファイルを作って、以下を貼ってください。

overwrite: true
schema: "http://localhost:3000/api/graphql"
documents: "graphql/**/*.graphql.ts"
generates:
  generated/graphql.ts:
    plugins:
      - "typescript"
      - "typescript-operations"
      - "typescript-graphql-request"

最後に documents に指定したファイルパス(この場合だと graphql/queries.graphql.ts みたいな)を作ってください。

一度この状態で npx graphql-codegen --watch と打ってみてください。

多分 GraphQL documents がないと言われ、何もされずに watch 状態になります。

では先ほど作った queries.graphql.ts に、以下を貼ってください。

import gql from "graphql-tag";

export const AllUsersQuery = gql`
  query allUsers {
    getAllUsers {
      name
    }
  }
`;

export const UserQuery = gql`
  query User($id: Int!) {
    user(where: { id: $id }) {
      id
      name
    }
  }
`;

すると、成功して generated/graphql.ts というファイルが生成されると思います。

一度あえて getAllUsersgetUsers みたいに間違えてセーブすると、ちゃんと
graphql-codegen がそんなクエリ nexus 側ではないよって言ってくれます。 Did you mean "getAllUsers", "users", or "user"? とも言われるので、間違えてもデバッグしやすいです。

それでは自動生成された generated/graphql.ts の SDK を使って、クライアント側で GraphQLClient のインスタンスを立てましょう。

まず、 lib フォルダをルートディレクトリに作り、そこに client.ts というファイルを作ります。

そして中身を、

import { GraphQLClient } from "graphql-request";
import { getSdk } from "../generated/graphql";

const API_ROOT = "/api/graphql";

const client = getSdk(new GraphQLClient(API_ROOT));

export default client;

これでクライアントはできました。あとは、リクエストを送る際、 lib/client からクライアントを import して、 fetch 代わりに使います。

例えば、

// pages/index.tsx

import client from "../lib/client";
import { useQuery } from "react-query";

export default function Home() {
  const { data, status } = useQuery("allUsers", () => client.AllUsers());

  if (status === "loading") return <div>Loading...</div>;

  return (
    <ul>
      {data.allUsers.map((user) => (
        <li>{user.name}</li>
      ))}
    </ul>
  );
}

これだけでレスポンスにちゃんと型情報がつくので、 nexus のスキーマだけが型情報の source of truth となり、メンテ地獄な二重管理とかは不要になります。

終わりに

GraphQL はまだまだ REST API に比べ採用率は低く感じますが、 TypeScript の普及によってもっと使われるようになるかもですね。

以上が Next.js と GraphQL Nexus を用いたアプリ開発のチュートリアルでした!

不明点などがあれば、いつでも twitter でご連絡ください。ありがとうございました。

Discussion