GraphQL Code GeneratorのServer Presetを紹介する
はじめに
nyatinteと申します!
普段はWeb開発やモバイルアプリのバックエンド開発などを行っております
私は普段よくGraphQLを使用して開発を行っているのですが、最近GraphQL Yogaを用いたAPI開発でServer Presetを使用し、快適に開発できているので、その紹介をしたいと思います
TL;DR
GraphQL Yoga, Apollo Server向けのServer Presetです
型安全性や、モジュール規則の強制により、スケールしやすいAPIを作成することができます
本記事で作成したサンプル実装のリポジトリはこちらです
どんな機能があるの?
型安全性
型付けされたリゾルバが生成されるため、開発の際のミスを防ぐことができます。
後述するMapperを使用することで、スキーマの型とマッパーの型を比較し、DBとAPIの間の型の差分を埋めることもできます
スキーマモジュールのベストプラクティスの強制
スキーマを小さなモジュールに分割し、保守性を高めています
ディレクトリ構成としては以下のようになります
├── src/
│ ├── schema/
│ │ ├── base/
│ │ │ ├── schema.graphql
│ │ ├── user/
│ │ │ ├── schema.graphql
│ │ ├── book/
│ │ │ ├── schema.graphql
1つのファイルに全てのスキーマを書くのではなく、モジュールごとに分割することで、スキーマの変更が容易になります
ファイル生成
パッケージを導入し、codegen.ts
にちょこっと設定を加えることで、スキーマの型定義や、リゾルバの型定義を生成することができます
最高ですね!
カスタムスカラーのサポート
同じくThe Guildが開発しているgraphql-scalarsとも簡単に連携することができます
マッパーの追加
例えば、User
に対するGraphQLのスキーマは以下のようになっているとします
type User {
id: ID!
firstName: String!
lastName: String!
fullName: String!
}
一方、DBから取得したUser
は以下のような型定義になっているとします
type User = {
id: string;
firstName: string;
lastName: string;
}
このような場合、User
を返却値にもつリゾルバをそのまま実装すると、fullName
が存在しないため、エラーが発生します
そのため、schema.mappers.ts
ファイルを用いて、User
をGraphQLのスキーマに合わせて変換することができます
実際に導入してみる
基本的にはこちらを参考に進めていけば問題ないと思います
本記事では、Prisma, Yogaなども導入し、詰まりがちなポイントについても解説していきます
1. プロジェクトの作成
今回は公式のexampleを使用します
せっかくなので、Bunを使用しているexampleを使用します
適当にCloneして、exampleの中からbun
ディレクトリを持ってくればOKです
いくつかパッケージが必要なので、インストールしておきます
bun add -D @graphql-codegen/cli @eddeee888/gcg-typescript-resolver-files prisma
bun add @prisma/client
2. prismaのセットアップ
bunx prisma init
Prismaスキーマを以下のように編集します
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
model User {
id String @id @default(uuid())
firstName String
lastName String
posts Post[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
今回はテスト用なので、sqlite
を使用します
.env
も以下のように編集します
DATABASE_URL="file:./dev.db"
ここでマイグレーションを実行します
bunx prisma migrate dev --name init
3. GraphQL Code Generator及びServer Presetのセットアップ
codegen.ts
を以下のように編集します
import type { CodegenConfig } from '@graphql-codegen/cli'
import { defineConfig } from '@eddeee888/gcg-typescript-resolver-files'
const config: CodegenConfig = {
schema: '**/schema.graphql',
generates: {
'src/schema': defineConfig()
}
}
export default config
Server Presetのスキーマモジュールのディレクトリ設計に従って、いくつかファイルを作成します
├── src/
│ ├── schema/
│ │ ├── base/
│ │ │ ├── schema.graphql
│ │ ├── user/
│ │ │ ├── schema.graphql
│ │ ├── post/
│ │ │ ├── schema.graphql
type Query
type Mutation
extend type Query {
user(id: ID!): User!
}
type User {
id: ID!
fullName: String!
posts: [Post!]!
}
extend type Query {
post(id: ID!): Post
}
extend type Mutation {
publishPost(id: ID!): Post
}
type Post {
id: ID!
title: String!
content: String
published: Boolean!
author: User!
}
ここまで作成できたら、ファイルを生成します
bunx graphql-codegen
src/schema
に以下のファイルが生成されていればOKです
src
├── index.ts
└── schema
├── base
│ └── schema.graphql
├── post
│ ├── resolvers
│ │ ├── Mutation
│ │ │ └── publishPost.ts
│ │ ├── Post.ts
│ │ └── Query
│ │ └── post.ts
│ └── schema.graphql
├── resolvers.generated.ts
├── typeDefs.generated.ts
├── types.generated.ts
└── user
├── resolvers
│ ├── Query
│ │ └── user.ts
│ └── User.ts
└── schema.graphql
最後に、src/index.ts
を以下のように編集します
import { createSchema, createYoga } from 'graphql-yoga';
import { typeDefs } from './schema/typeDefs.generated'
import { resolvers } from './schema/resolvers.generated'
const yoga = createYoga({
schema: createSchema({
typeDefs,
resolvers
}),
});
const server = Bun.serve(yoga);
console.info(
`Server is running on http://${server.hostname}:${server.port}${yoga.graphqlEndpoint}`,
);
typeDefsとresolversをimportして、createSchema
に渡しています
4. リゾルバの実装
*/resolvers/*.ts
の実装をしていきます
a. contextにPrismaを追加する
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export type GraphQLContext = {
prisma: PrismaClient;
};
export function createContext(): GraphQLContext {
return {
prisma,
};
}
import { createSchema, createYoga } from 'graphql-yoga';
import { typeDefs } from './schema/typeDefs.generated';
import { resolvers } from './schema/resolvers.generated';
import { createContext } from './context';
const yoga = createYoga({
schema: createSchema({
typeDefs,
resolvers,
}),
context: createContext(),
});
const server = Bun.serve(yoga);
console.info(
`Server is running on http://${server.hostname}:${server.port}${yoga.graphqlEndpoint}`
);
これでContextにPrismaを追加することができました
しかし、自動生成されたリゾルバにおけるcontext
の型はany
になっています
これを修正するために、codegen.ts
を以下のように編集します
import type { CodegenConfig } from '@graphql-codegen/cli';
import { defineConfig } from '@eddeee888/gcg-typescript-resolver-files';
const config: CodegenConfig = {
schema: '**/schema.graphql',
generates: {
'src/schema': defineConfig({
typesPluginsConfig: {
contextType: '../context#GraphQLContext',
},
}),
},
};
export default config;
これで、自動生成されたリゾルバのcontext
の型がGraphQLContext
になりました
Prismaを型安全に扱えますね!
b. リゾルバの実装
import type { QueryResolvers } from './../../../types.generated';
export const user: NonNullable<QueryResolvers['user']> = async (
_parent,
{ id },
{ prisma }
) => {
const user = prisma.user.findUniqueOrThrow({ where: { id } });
return {
__typename: 'User',
...user,
};
};
一見良さそうですが、このままでは型エラーが起きてしまいます。
型 {
id: string;
firstName: string;
lastName: string;
createdAt: Date;
updatedAt: Date;
}
には
型 User からの次のプロパティがありません
fullName, posts
c. マッパーの実装
これを解消するためにMapperを設定します。
src/schema/user/user.mappers.ts
を以下のように編集します
export { User as UserMapper } from '@prisma/client';
{GraphQLのType名}Mapper
としてexportすればOKです
ついでにPostのMapperも作成します
export type { Post as PostMapper } from '@prisma/client';
src/schema/user/resolvers/Query/user.ts
で起きていた型エラーも通るようになりました
しかしこのままだと未定義のフィールドリゾルバーにおける処理が記述されていないので修正します
import type { UserResolvers } from './../../types.generated';
export const User: UserResolvers = {
fullName: (parent) => `${parent.firstName} ${parent.lastName}`,
posts: (parent, _, { prisma }) => {
return prisma.user.findUnique({ where: { id: parent.id } }).posts();
},
};
import type { PostResolvers } from './../../types.generated';
export const Post: PostResolvers = {
author: (parent, _, { prisma }) => {
return prisma.post.findUnique({ where: { id: parent.id } }).author();
},
};
これでUserを取得するQueryの実装は完了です!
Mutationの実装は本記事では省略するので、気になる方はサンプルリポジトリをご覧ください
5. 動かしてみる
Prisma Studioでサンプルユーザー作り、Queryを投げてみます
bunx prisma studio
いい感じに動作していそうです!
まとめ
この記事ではGraphQL Code GeneratorのServer Presetを紹介しました
npmのinstall数もまだまだ少ないですが、公式も紹介している手法なので今後が楽しみですね!
ほかにもこんなpresetがあるよ!というのがあれば教えていただけると嬉しいです
Discussion