🛰️

Apollo Server v4、Next.js、Prisma でGraphQLのセットアップ

2023/03/25に公開

はじめに

apollo-server-microが廃止予定になっていてApollo Server v4を使うようになっていたので、Apollo Server v4Next.jsPrisma を使った環境構築の覚書です。

リポジトリ:https://github.com/t-shiratori/react-query-rest-graphql-prisma

apollo-server-micro は廃止予定

apollo-server-micro

This package has been deprecated
Author message:
The apollo-server-micro package is part of Apollo Server v2 and v3, which > are now deprecated (end-of-life October 22nd 2023). This package's > functionality is now found in the @apollo/server package. See > https://www.apollographql.com/docs/apollo-server/previous-versions/ for more > details.

  • apollo-server-microパッケージは、2023年10月22日に廃止予定で現在は非推奨
  • このパッケージの機能は、@apollo/serverパッケージに含まれるようなった
  • 詳細については、Previous versions of Apollo Server を参照

Apollo Server 4へのマイグレーションの記事
Migrating to Apollo Server 4

Next.jsでプロジェクトを作成

https://nextjs.org/docs/api-reference/create-next-app

npx create-next-app@latest

Prismaのセットアップ

https://www.prisma.io/docs/getting-started/setup-prisma/add-to-existing-project/relational-databases-typescript-postgres

prismaをインストール

npm install prisma --save-dev

@prisma/clientをインストール

呼び出し部分の実装で使うので入れておきます。

npm i @prisma/client

参考: https://www.prisma.io/docs/concepts/components/prisma-client

初期化

https://www.prisma.io/docs/reference/api-reference/command-reference#init

npx prisma init

スキーマの定義

prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

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

model Task {
  id           Int      @id @default(autoincrement())
  title        String?
  content      String?
}

ちなみにスキーマのファイル分割についてはだいぶ前にイシューが立っていて今だに議論中のようでした。

https://github.com/prisma/prisma/issues/2377

DBと接続する

datasource dbのurlに使用するデータベースのURLを指定します。
自分は使い勝手がいいのでsupabaseを使いました。
https://www.prisma.io/docs/reference/database-reference/connection-urls

マイグレーション

npx prisma migrate dev

prisma studio を起動する

https://www.prisma.io/studio

npx prisma studio

DBと繋がっていれば以下のように prisma studio が表示されます。

REST API用のデータソースの定義

src/pages/api/graphql/datasources/jsonplaceholder-use.ts
import { RESTDataSource } from '@apollo/datasource-rest'

export class JsonplaceholderUserApi extends RESTDataSource {
  override baseURL = 'https://jsonplaceholder.typicode.com/'

  async getUsers() {
    return this.get('users')
  }

  async getUser(id: string) {
    return this.get(`users/${id}`)
  }
}
src/pages/api/graphql/datasources/jsonplaceholder-post.ts
import { RESTDataSource } from '@apollo/datasource-rest'

export class JsonplaceholderPostApi extends RESTDataSource {
  override baseURL = 'https://jsonplaceholder.typicode.com/'

  async getPosts() {
    return this.get('posts')
  }

  async getPost(id: string) {
    return this.get(`posts/${id}`)
  }
}

jsonplaceholderを使用しています。

Graphqlスキーマの定義

ファイル分割して定義します。以下を参考にしました。
https://the-guild.dev/graphql/tools/docs/schema-loading

src/pages/api/graphql/schema/root.graphql
#import prismaUser, createPrismaUserInput from "prismaUser.graphql"
#import jsonplaceholderPost from "jsonplaceholderPost.graphql"
#import jsonplaceholderUser from "jsonplaceholderUser.graphql"

type Query {
  hello: String
  prismaUser(id: ID!): prismaUser
  prismaUsers: [prismaUser]
  jsonplaceholderUser(id: ID!): jsonplaceholderUser
  jsonplaceholderUsers: [jsonplaceholderUser]
  jsonplaceholderPost(id: ID!): jsonplaceholderPost
  jsonplaceholderPosts: [jsonplaceholderPost]
}

type Mutation {
  prismaUser(user: createPrismaUserInput!): prismaUser
}
src/pages/api/graphql/schema/jsonplaceholderUser.graphql
#import jsonplaceholderPost from "jsonplaceholderPost.graphql"

type Company {
  name: String
  catchPhrase: String
  bs: String
}
type Geo {
  lat: String
  lng: String
}
type Address {
  street: String
  suite: String
  city: String
  zipcode: String
  geo: Geo
}
type jsonplaceholderUser {
  id: Int
  name: String
  username: String
  email: String
  phone: String
  website: String
  company: Company
  address: Address
  posts: [jsonplaceholderPost]
}
src/pages/api/graphql/schema/jsonplaceholderPost.graphql
type jsonplaceholderPost {
  userId: Int
  id: Int
  title: String
  body: String
}
src/pages/api/graphql/schema/prismaUser.graphql
type prismaUser {
  id: ID!
  name: String!
  email: String!
}

input createPrismaUserInput {
  name: String!
  email: String!
}

スキーマからTypeScriptの型定義を生成

以下が参考になりました。
https://the-guild.dev/graphql/codegen/plugins/typescript/typescript-resolvers

パッケージをインストール

下記のパッケージを使います。

  • @graphql-codegen/cli
  • @graphql-codegen/typescript
  • @graphql-codegen/typescript-resolvers
npm install -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-resolvers

設定ファイルを作成

codegen.tsを作って以下のように定義します。

codegen.ts
import type { CodegenConfig } from '@graphql-codegen/cli'

const config: CodegenConfig = {
  overwrite: true,
  schema: 'src/pages/api/graphql/schema/root.graphql',
  generates: {
    'src/pages/api/graphql/types/graphql.ts': {
      plugins: ['typescript', 'typescript-resolvers'],
      config: {
        mappers: {
          prismaUser: '@prisma/client/index.d#User',
        },
      },
    },
  },
}

export default config

mappersにprisma用の指定をしています。GraphQLスキーマから生成されたデフォルトの型だとエラーになるので、代わりにPrismaのモデルを使用するようにcodegenに指示しています。

参考: GraphQL Code Generator with TypeScript and Prisma models – The Guild

コマンドで生成

npx graphql-codegen --config codegen.ts

src/pages/api/graphql/schema/root.graphqlを元にしてsrc/pages/api/graphql/types/graphql.tsにtypescriptの型定義が出力されます。

リゾルバの定義

src/pages/api/graphql/resolver/index.ts
import { PrismaClient } from '@prisma/client'
import { JsonplaceholderPost, Resolvers } from '../types/graphql'

const prisma = new PrismaClient()

export const resolvers: Resolvers = {
  Query: {
    hello: () => 'Hello World',
    prismaUser: async (_, args, ___, ____) => {
      const result = await prisma.user.findUnique({
        where: {
          id: Number(args.id),
        },
      })
      return result
    },
    prismaUsers: () => prisma.user.findMany(),
    jsonplaceholderUser: (_, args, contextValue) => contextValue.dataSources.jsonplaceholderUserApi.getUser(args.id),
    jsonplaceholderUsers: (_, __, contextValue) => contextValue.dataSources.jsonplaceholderUserApi.getUsers(),
    jsonplaceholderPost: (_, args, contextValue) => contextValue.dataSources.jsonplaceholderPostApi.getPost(args.id),
    jsonplaceholderPosts: (_, __, contextValue) => contextValue.dataSources.jsonplaceholderPostApi.getPosts(),
  },
  Mutation: {
    prismaUser: async (_, args, ___, ____) => {
      const { name, email } = args.user
      const result = await prisma.user.create({
        data: {
          email,
          name,
        },
      })
      return result
    },
  },
  jsonplaceholderUser: {
    posts: async (parent, _, contextValue) => {
      const allPosts: JsonplaceholderPost[] = await contextValue.dataSources.jsonplaceholderPostApi.getPosts()
      const filteredPosts: JsonplaceholderPost[] = allPosts.filter(({ userId }) => userId === parent.id)
      return filteredPosts
    },
  },
}

Next.jsのAPIルートにApolloServerを設置する

@as-integrations/nextが用意されているのでそれを使うと簡単に書けます。

@graphql-tools/load@graphql-tools/graphql-file-loaderは、@graphql-codegen/cliをインストールすると一緒にインストールされます。

loadSchemaSyncで外部化したスキーマファイルを同期的に読み込むようにします。

src/pages/api/graphql/index.ts
import { ApolloServer } from '@apollo/server'
import { startServerAndCreateNextHandler } from '@as-integrations/next'
import { JsonplaceholderPostApi } from './datasources/jsonplaceholder-post'
import { JsonplaceholderUserApi } from './datasources/jsonplaceholder-use'
import { loadSchemaSync } from '@graphql-tools/load'
import { GraphQLFileLoader } from '@graphql-tools/graphql-file-loader'
import { resolvers } from './resolver'

const typeDefs = loadSchemaSync('src/pages/api/graphql/schema/root.graphql', {
  loaders: [new GraphQLFileLoader()],
})

type TContextValue = {
  dataSources: {
    jsonplaceholderUserApi: JsonplaceholderUserApi
    jsonplaceholderPostApi: JsonplaceholderPostApi
  }
}

const server = new ApolloServer<TContextValue>({
  typeDefs,
  resolvers,
})

export default startServerAndCreateNextHandler(server, {
  context: async () => ({
    dataSources: {
      jsonplaceholderUserApi: new JsonplaceholderUserApi(),
      jsonplaceholderPostApi: new JsonplaceholderPostApi(),
    },
  }),
})

Apollo Explorer で確認

アプリを起動します。

yarn dev

http://localhost:3000/api/graphqlにアクセスすると以下のように表示されます。

Discussion