🚀

NextjsのRoute HandlersにGraphQLサーバー(Apollo)を設定&クライアント側で呼び出し

2024/10/06に公開

やりたいこと

GraphQLサーバー&クライアント両方の仕組みを持ったNextjsアプリサンプルを構築するために、Route HandlersにGraphQLサーバー(Apollo)を設定します。

動機

GraphQLは非常に使い勝手が良く、積極的にNextjsに取り入れたいと思っています。また、NextjsにはRoute Handlersという仕組みがあります。src/app/api配下にroute.tsを作成し、GETやPOSTと言う名前でfunctionをexportすると指定したフォルダパスに対応したメソッドのRESTAPIが作成されます。こちらにGraphQLを指定することでGraphQLサーバーを実現します(具体的な方法は後述します)。以前CustomServerでGraphQLサーバー&クライアントを実現したのですが、Nextjsのレンダリング最適化が失われてしまい非常に重いアプリができてしまいました。。ここは公式に則ってRoute HandlersでGraphQLサーバーを実現したいと思います。

公式

Nextjs公式が出しているGraphQL+APIRouteのサンプルはこちらです。
https://github.com/vercel/next.js/tree/canary/examples/api-routes-graphql
こちらはgraphql-yogaを使った実装となっています。
Apolloについては示されていなかったので調べたところ@as-integrations/nextが使えるようです。

@as-integrations/next

Apolloについては@as-integrations/nextが使えるようです。

https://www.npmjs.com/package/@as-integrations/next

こちらの公式を見ると

import { startServerAndCreateNextHandler } from "@as-integrations/next";
・・・
const server = new ApolloServer({
  resolvers,
  typeDefs,
});
export default startServerAndCreateNextHandler(server)

とstartServerAndCreateNextHandlerをexportしてRoute HandlersでこちらをGETとPOSTでreturnしてあげることでGraphQLが利用できるようになるようです。簡単ですね。

サンプル

※GraphQLではDB(postgresqlのneon)に接続するようになっていますがここでは省略します。

src/app/api/graphql/route.ts

import { db } from '@/server/db'
import GraphQL from '@/server/graphql'

const graphql = new GraphQL(db)

export async function GET(req: Request) {
  return graphql.handler(req)
}

export async function POST(req: Request) {
  return graphql.handler(req)
}

src/server/graphql/index.ts

/* eslint-disable @typescript-eslint/no-unused-vars */
import { ApolloServer } from '@apollo/server'
import { startServerAndCreateNextHandler } from "@as-integrations/next";
import { makeExecutableSchema } from "@graphql-tools/schema";
import type { NeonHttpDatabase } from 'drizzle-orm/neon-http'
import ToDo, {
  CreateTodoInputType,
  DeleteTodoInputType,
  UpdateTodoInputType,
} from '@/server/graphql/model/todo'
import { NextApiRequest, NextApiResponse } from 'next'

class GraphQL {
  private todo: ToDo

  constructor(db: NeonHttpDatabase<Record<string, never>>) {
    this.todo = new ToDo(db)
  }

  private typeDefs = `
  type Todo {
    taskId: Int!
    contents: String
  }
  
  input CreateTodoInput {
    contents: String!
  }

  input UpdateTodoInput {
    taskId: Int!
    contents: String!
  }

  input DeleteTodoInput {
    taskId: Int!
  }

  type UpdateTodoOutput {
    taskId: Int!
    contents: String!
  }

  type DeleteTodoOutput {
    taskId: Int!
  }

  type Query {
    getTodos: [Todo]
  }

  type Mutation {
    createTodo(input: CreateTodoInput): [Todo]
    updateTodo(input: UpdateTodoInput): UpdateTodoOutput
    deleteTodo(input: DeleteTodoInput): DeleteTodoOutput
  }
  `
  private resolvers = {
    Query: {
      getTodos: async (_root: unknown, {}, _req: NextApiRequest) => {
        return this.todo.getToDos()
      },
    },
    Mutation: {
      createTodo: async (
        _root: unknown,
        params: CreateTodoInputType,
        _req: NextApiRequest,
      ) => {
        return this.todo.createToDo(params)
      },
      updateTodo: async (
        _root: unknown,
        params: UpdateTodoInputType,
        _req: NextApiRequest,
      ) => {
        const res = await this.todo.updateToDo(params)
        return params.input
      },
      deleteTodo: async (
        _root: unknown,
        params: DeleteTodoInputType,
        _req: NextApiRequest,
      ) => {
        await this.todo.deleteToDo(params)
        return params.input
      },
    },
  }

  public handler = startServerAndCreateNextHandler(
    new ApolloServer({
      schema: makeExecutableSchema({
        typeDefs: this.typeDefs,
        resolvers: this.resolvers,
      }),
    }),
    {
      context: async (req: NextApiRequest) => req,
    },
  )
}
export default GraphQL

クライアントコンポーネント

'use client'

import { client } from '@/util/apollo/csrClient'
import { ApolloProvider } from '@apollo/client'

import { Crud } from '@/app/components/pages/labo/nextjs/crud/Crud'

export default function CrudPage() {
  return (
    <ApolloProvider client={client}>
      <Crud />
    </ApolloProvider>
  )
}

↑のCrudコンポーネントの中で

export function CrudList() {
  const { loading, error, data } = useQuery(GetTodosDocument, {
    fetchPolicy: 'cache-and-network',
  })
  return <> ・・・</>
}

こんな感じで呼び出せます。

Discussion