For

2023.12.31

Next.js + GraphQL Yoga + Pothos + Prisma + GraphQL Code Generatorで開発環境を構築する

概要

Nexusの更新が滞ってしまっている件について、業務上で関わりのあったエンジニアさんから、Pothosが代替ツールとなりそうという話を伺ったのと、個人的にもTypeScriptでGraphQLスキーマを管理できる方が良いと思っているので、以下のフレームワーク、ライブラリでの開発環境を構築してみます。

・Next.js(Production環境を想定してPages Routingを使います)
・GraphQL Yoga
・Pothos
・Prisma
・GraphQL Code Generator
・URQL

手順

Next.jsアプリケーションを作成

適当に yarn create next-app . する。
なお、App RouterはまだProduction環境で使いたくないので、今回はPages Routerを使います。

パッケージインストール

bash_____terminal_____yarn add @pothos/core @pothos/plugin-prisma @prisma/client graphql graphql-yoga urql


bash_____terminal_____yarn add -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typescript-urql prisma


package.jsonを編集

JSON_____package.json_____  "resolutions": {
    "jackspeak": "2.1.1"
  }


こちらについては以下の記事に理由が書いてあります。
タイミングによっては不要な場合もあると思いますが2023/12/31時点では必要でした。
For - Pothos + GraphQL Code Generatorでrequire('string-width')がERR_REQUIRE_ESMになったら試すこと

編集が終わったらパッケージをインストールし直します。

bash_____terminal_____rm yarn.lock
rm -fr node_modues
yarn


docker関連のファイルを作成

今回はNext.jsとDBのコンテナを立てて進めるため、以下のように/docker/docker-compose.ymlを作成します。

yml_____docker-compose.yml_____version: '3'

volumes:
  node_modules:
  postgres:

services:
  app:
    image: node:20
    ports:
      - 3101:3101
      - 5555:5555
    volumes:
      - ../:/workspace:cached
      - node_modules:/workspace/node_modules
    working_dir: /workspace
    tty: true
  db:
    image: postgres:14
    environment:
      POSTGRES_DB: database
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
    volumes:
      - postgres:/var/lib/postgresql/data
    ports:
      - 5432:5432


これはお好みですが私はVSCodeのDev Containersで進めるので必要なファイルを作ります。

/.devcontainer/devcontainer.json

JSON_____devcontainer.json_____{
  "name": "sample-app-dev-container",
  "dockerComposeFile": ["../docker/docker-compose.yml"],
  "service": "app",
  "workspaceFolder": "/workspace",
  "customizations": {
    "vscode": {
      "extensions": [
        "prisma.prisma",
        "esbenp.prettier-vscode"
      ]
    }
  }
}


docker compose up -d したらVSCodeのDev Containerからコンテナの中に入って作業します。

prismaを初期化

bash_____terminal_____npx prisma init


.env の値を編集します。

text_____.env_____DATABASE_URL="postgresql://postgres:password@db:5432/database?schema=public&connect_timeout=300"


schema.prismaを編集

pothosのgeneratorと適当なmodelを追加します。

prisma_____schema.prisma_____generator client {
  provider = "prisma-client-js"
}

generator pothos {
  provider = "prisma-pothos-types"
  clientOutput = ".prisma/client"
  output = "../src/generated/pothos-types.ts"
}

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

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

model Post {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  title     String
  content   String?
  published Boolean  @default(false)
  viewCount Int      @default(0)
  author    User?    @relation(fields: [authorId], references: [id])
  authorId  Int?
}


generator clientのデフォルトの出力先はnode_modules/.prisma/clientなので、そこに合わせてclientOutputは.prisma/clientにします。
yarn prisma:migrate:reset しておきます。

PrismaClientを作成

schema.prismaに追加したgenerator pothosのclientOutputで設定したディレクトリからPrismaClientをimportしてclientを作成します。
/src/backend/db.ts

TypeScript_____db.ts_____import { PrismaClient } from '../../node_modules/.prisma/client';

export const prisma = new PrismaClient();


Schemaを作成

PothosのSchemaBuilderでschemaを作成します。
/src/backend/builder.ts

TypeScript_____builder.ts_____import SchemaBuilder from '@pothos/core';
import PrismaPlugin from '@pothos/plugin-prisma';
import type PrismaTypes from '@/generated/pothos-types';
import { prisma } from './db';

export const builder = new SchemaBuilder<{
  Context: { userId: number };
  PrismaTypes: PrismaTypes;
}>({
  plugins: [PrismaPlugin],
  prisma: {
    client: prisma,
  },
});

builder.queryType({});
// builder.mutationType({});


クエリフィールドを作成

適当なクエリフィールドを作成します。
/src/backend/types/hello.ts

TypeScript_____hello.ts_____import { builder } from '../builder';

builder.queryField('hello', (t) => {
  return t.string({
    args: {
      name: t.arg.string(),
    },
    resolve: (parent, { name }) => `hello, ${name || 'World'}`,
  });
});


Schemaをエクスポート

作成したクエリフィールドをインポートし、schemaとしてエクスポートします。
/src/backend/schema.ts

TypeScript_____schema.ts_____import { builder } from './builder';
import './types/hello';

export const schema = builder.toSchema();


APIルートを作成

GraphQLのAPIルートを作成します。
/src/pages/api/graphql.ts

TypeScript_____index.tsx_____import { schema } from '@/backend/schema';
import { createYoga } from 'graphql-yoga';
import type { NextApiRequest, NextApiResponse } from 'next';

export type Context = {
  res: NextApiResponse;
  req: NextApiRequest;
};

export default createYoga<
  {
    req: NextApiRequest;
    res: NextApiResponse;
  },
  Context
>({
  graphqlEndpoint: '/api/graphql',
  schema,
  context: ({ req, res }) => ({ req, res }),
});


GraphiQLで確認

yarn dev したら localhost:3000/api/graphql にアクセスして確認します。

リクエストを作成

動作が確認できたので、フロントエンドからのリクエストの部分を作っていきます。

GraphQL_____hello.graphql_____query Hello {
  hello
}


codegen.tsを作成

GraphQL Code Generatorの設定ファイルを作成します。
今回はURQLというGraphQLクライアントをフロントエンドで使用するので、それに合った設定をします。

TypeScript_____codegen.ts_____import { schema } from './src/backend/schema';
import type { CodegenConfig } from '@graphql-codegen/cli';
import { printSchema } from 'graphql';

const config: CodegenConfig = {
  schema: printSchema(schema),
  overwrite: true,
  documents: ['src/graphql/*.graphql'],
  emitLegacyCommonJSImports: false,
  generates: {
    'src/generated/graphql.ts': {
      plugins: ['typescript', 'typescript-operations', 'typescript-urql'],
      config: {
        scalars: {
          DateTime: 'string',
        },
      },
    },
  },
};

export default config;


URQLのclientをprovideする

/src/pages/_app.tsx

tsx_____ _app.tsx_____import '@/styles/globals.css';
import type { AppProps } from 'next/app';
import { cacheExchange, fetchExchange, createClient, Provider } from 'urql';

export default function App({ Component, pageProps }: AppProps) {
  const urqlClient = createClient({
    exchanges: [cacheExchange, fetchExchange],
    url: 'http://localhost:3000/api/graphql',
  });
  return (
    <Provider value={urqlClient}>
      <Component {...pageProps} />
    </Provider>
  );
}


続いて yarn graphql-codegen します。

text_____terminal_____root@0e2e0a9368a3:/workspace# yarn graphql-codegen
yarn run v1.22.19
$ graphql-codegen
✔ Parse Configuration
✔ Generate outputs
Done in 0.73s.


成功したので、URQLのQueryのReact用Hooksが生成されました。

生成したHooksを使う

最後に生成されたHooksを使って確認してみます。
/src/pages/index.tsx

tsx_____index.tsx_____// ~~

export default function Home() {
  const [{data}] = useHelloQuery();
  console.log(data?.hello);

// ~~



コンソールでリクエストの結果が確認できました。

まとめ

これで一通り基礎的な環境は構築できたと思うので、これをベースにもう少し触っていきたいです。
環境構築をやるたびに何かしらの勉強になります。