🔖

GraphQL Code GeneratorのServer Presetを紹介する

2023/09/23に公開

はじめに

nyatinteと申します!
普段はWeb開発やモバイルアプリのバックエンド開発などを行っております
私は普段よくGraphQLを使用して開発を行っているのですが、最近GraphQL Yogaを用いたAPI開発でServer Presetを使用し、快適に開発できているので、その紹介をしたいと思います

TL;DR

GraphQL Yoga, Apollo Server向けのServer Presetです
型安全性や、モジュール規則の強制により、スケールしやすいAPIを作成することができます

https://the-guild.dev/graphql/codegen/docs/guides/graphql-server-apollo-yoga-with-server-preset

本記事で作成したサンプル実装のリポジトリはこちらです
https://github.com/nyatinte/bun-yoga-server-preset-test

どんな機能があるの?

型安全性

型付けされたリゾルバが生成されるため、開発の際のミスを防ぐことができます。
後述するMapperを使用することで、スキーマの型とマッパーの型を比較し、DBとAPIの間の型の差分を埋めることもできます

スキーマモジュールのベストプラクティスの強制

スキーマを小さなモジュールに分割し、保守性を高めています
ディレクトリ構成としては以下のようになります

├── src/
│   ├── schema/
│   │   ├── base/
│   │   │   ├── schema.graphql
│   │   ├── user/
│   │   │   ├── schema.graphql
│   │   ├── book/
│   │   │   ├── schema.graphql

1つのファイルに全てのスキーマを書くのではなく、モジュールごとに分割することで、スキーマの変更が容易になります

ファイル生成

パッケージを導入し、codegen.tsにちょこっと設定を加えることで、スキーマの型定義や、リゾルバの型定義を生成することができます
最高ですね!

カスタムスカラーのサポート

同じくThe Guildが開発しているgraphql-scalarsとも簡単に連携することができます

マッパーの追加

例えば、Userに対するGraphQLのスキーマは以下のようになっているとします

schema/user/schema.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を使用します

https://github.com/dotansimha/graphql-yoga/tree/8eb1d8038aa693059b51381d2e961cb730eb19d7/examples/bun

せっかくなので、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スキーマを以下のように編集します

prisma/schema.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も以下のように編集します

.env
DATABASE_URL="file:./dev.db"

ここでマイグレーションを実行します

sh
bunx prisma migrate dev --name init

3. GraphQL Code Generator及びServer Presetのセットアップ

codegen.tsを以下のように編集します

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
src/schema/base/schema.graphql
type Query
type Mutation
src/schema/user/schema.graphql
extend type Query {
  user(id: ID!): User!
}
type User {
  id: ID!
  fullName: String!
  posts: [Post!]!
}
 
src/schema/post/schema.graphql
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!
}

ここまで作成できたら、ファイルを生成します

sh
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を以下のように編集します

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を追加する

src/context.ts
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

export type GraphQLContext = {
  prisma: PrismaClient;
};

export function createContext(): GraphQLContext {
  return {
    prisma,
  };
}
src/index.ts
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を以下のように編集します

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. リゾルバの実装

src/schema/user/resolvers/Query/user.ts
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を以下のように編集します

src/schema/user/schema.mappers.ts
export { User as UserMapper } from '@prisma/client';

{GraphQLのType名}MapperとしてexportすればOKです

ついでにPostのMapperも作成します

src/schema/post/schema.mappers.ts
export type { Post as PostMapper } from '@prisma/client';

src/schema/user/resolvers/Query/user.tsで起きていた型エラーも通るようになりました

しかしこのままだと未定義のフィールドリゾルバーにおける処理が記述されていないので修正します

src/schema/user/resolvers/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();
  },
};
src/schema/post/resolvers/Post.ts
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を投げてみます

sh
bunx prisma studio

いい感じに動作していそうです!

まとめ

この記事ではGraphQL Code GeneratorのServer Presetを紹介しました
npmのinstall数もまだまだ少ないですが、公式も紹介している手法なので今後が楽しみですね!
ほかにもこんなpresetがあるよ!というのがあれば教えていただけると嬉しいです

GitHubで編集を提案

Discussion