Next.js + GraphQL Nexus で作る超絶 typesafe なサーバーレス GraphQL フルスタックアプリケーションの手引き
どうも、@yuyaaar です。
先日書いた記事がありがたいことにとても反響があり、嬉しい限りです。皆さんありがとうございます。
今回は、その書いた記事の延長版を書きたいと思います。
何がどう延長なのか?
昨日書いたチュートリアルは、クラシックな 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 schemanexus-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.ts
と graphql/context.ts
を作ります。
// graphql/schema.tsimport { 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.tsimport { 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.tsimport { 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.tstype Query { users( first: Int last: Int before: UserWhereUniqueInput after: UserWhereUniqueInput ): [User!]! user(where: UserWhereUniqueInput!): User}input UserWhereUniqueInput { id: Int}
crud
だけではなく、カスタマイズもできます。例えば、
// graphql/schema.tsconst 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.tsconst 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: trueschema: "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
というファイルが生成されると思います。
一度あえて getAllUsers
を getUsers
みたいに間違えてセーブすると、ちゃんと
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.tsximport 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 となり、メンテ地獄な二重管理とかは不要になります。
Conclusion (終わりに)
GraphQL はまだまだ REST API に比べ採用率は低く感じますが、 TypeScript の普及によってもっと使われるようになるかもですね。
以上が Next.js と GraphQL Nexus を用いたアプリ開発のチュートリアルでした!
不明点などがあれば、いつでも twitter でご連絡ください。ありがとうございました。