【GraphQL in Nextjs】nexusでGraphQLスキーマ設計を効率的に行おう!
こんにちは、@nerusanです。
今回は、GraphQLのスキーマ設計でnexusが便利だったので紹介しようと思います。
通常、GraphQLを利用する時は、スキーマ定義言語(Schma Definition Language)を使用してスキーマを定義し、データを提供するためのリゾルバーロジックを記述します。
例えば、タスク一覧のデータを返すAPIを考えると、以下のようになります。
import { gql } from 'apollo-server-micro'
// スキーマ定義
// タスクを扱う型
export const typeDefs = gql`
type Task {
id: Int
title: String
done: Boolean
}
type Query {
tasks: [Task]!
}
`
// リゾルバーの記述
// 一旦JSONの固定値を返すロジックにしている
export const resolvers = {
Query: {
tasks: () => {
return [
{
id: 1,
title: 'task 1',
done: false,
},
{
id: 2,
title: 'task 2',
done: false,
},
]
}
}
}
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は、エラーになるため、すぐ気づく
詳しくは公式ページをご覧ください。
それでは、実際に使ってみましょう!
nexusを使ってみよう
まずは、インストールします。
$ yarn add nexus
スキーマとリゾルバーを定義していきます。
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,
},
]
},
});
},
});
全てエクスポートします。
export * from './Task'
設定などを記述します。
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で設定
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を利用し、コード補完が効くため、コードも素早く書けます。また、タイポもすぐ教えてくれます。例えば以下のようなミスもすぐにエラーが表示されます。
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の値を返すこともできます。
以下のように設定します。
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;
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,
};
}
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"),
+ },
})
そうするとリゾルバーで以下のようにアクセスできる。
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(); // データベースの値を返す
},
});
},
});
詳しくは公式のサンプルコードをご覧いただくといいです。
まとめ
どうでしたでしょうか。コードファーストにかけ、TypeScriptの恩恵も受けれることができるので、とても楽になります。
GraphQLを利用する際は是非積極的に利用を検討したいツールですね。
参考
Discussion