🛠️

ゼロからGraphQLと簡単なWebアプリの練習環境を整える

2024/07/26に公開

最近、少しだけ GraphQL に触れる機会がありました。過去に少しだけ経験したことがありましたが、久しぶりに練習しようと思い立ち、GraphQL を使う環境と具体的にどう使えるのか確認するための Web アプリを作成してみました。

今回試した環境

  • MacBook Air M1 (Darwin Kernel Version 23.5.0)
  • orbstack: 1.6.4_17192
  • docker: Docker version 26.1.3, build b72abbb
  • docker compose: Docker Compose version v2.27.3

今回作るお試しシステム環境について

まず、どのようなシステム構成でお試し環境を作るかを説明します。目標は以下の通りです。

構築するアーキテクチャ

一般的な 3 層アーキテクチャを docker compose で実現します。

せっかくなので、GraphQL を便利に使いこなすため、今回は少し複雑な場合を想定してみます。具体的には、ドキュメント DB である MongoDB とリレーショナル DB である PostgreSQL の両方をデータソースとして統合し、GraphQL から操作できる環境を作ります。
今回は効率や理想的な設計については考慮せず、GraphQL が PostgreSQL や MongoDB など異なる種類の DB を扱えるよということを確認できることを目標とします。

想定するシステムの要件と動作

なるべく単純にして下記を想定します。

  1. 書店は PostgreSQL で管理されている
  2. 本は MongoDB で管理されている
  3. 書店と本のつながりは一旦 PostgreSQL で管理する
  4. DB に対する操作として Create と Read のみを想定する
  5. ユーザーは web アプリから下記操作できる
    • 本一覧を表示する
    • 書店を検索して書店にある本の在庫を表示する
    • 書店に本の在庫を追加する

PostgreSQL と MongoDB の ER 図は下記のとおりです。

ER図

GraphQL を MongoDB と PostgreSQL で利用したい

今回作る構成

├── docker-compose.yml ... mongo, postgresql, graphql server, web appを検証として立ち上げるためのdocker compose
├── mongodb ... mongodb のデータ保管用
├── postgresql ... postgresql のデータ保管用
├── server ... GraphQL Server (Apollo Server)
│   ├── Dockerfile
│   ├── dist
│   ├── node_modules
│   ├── package-lock.json
│   ├── package.json
│   ├── src
│   └── tsconfig.json
└── web-client ... webアプリ(react-vite)
    ├── Dockerfile
    ├── README.md
    ├── index.html
    ├── package-lock.json
    ├── package.json
    ├── public
    ├── src
    ├── tsconfig.app.json
    ├── tsconfig.json
    ├── tsconfig.node.json
    └── vite.config.ts

Apollo Server と MongoDB の環境を作る

まずは server ディレクトリを作成して、Apollo Serverのチュートリアルに従ってセットアップします。

上記サイト Step 8 まで実行したら、今後の作業のために index.ts を少し整理して全体的にわかりやすくします。

まずは typeDef.ts の内容を切り出して、schema.graphql として切り出します。想定しているシステムでは「本」に対して Create と Read を行いたいので、 GraphQL の mutation を追加します。

type Book {
  title: String
  author: String
}

type Query {
  books: [Book]
}

type AddBookMutationResponse {
  code: String!
  success: Boolean!
  message: String!
  book: Book
}

type Mutation {
  addBook(title: String, author: String): AddBookMutationResponse
}

GraphQL のスキーマから自動的に TypeScript のタイプを生成するために、追加のパッケージをインストールして、初期設定します(参考)。

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

➜ npx graphql-code-generator init

    Welcome to GraphQL Code Generator!
    Answer few questions and we will setup everything for you.

? What type of application are you building? Backend - API or server
? Where is your schema?: (path or url) ./schema.graphql
? Pick plugins: TypeScript (required by other typescript plugins), TypeScript Resolvers (strongly typed resolve functions)
? Where to write the output: src/generated/graphql.ts
? Do you want to generate an introspection file? Yes
? How to name the config file? codegen.ts
? What script in package.json should run the codegen? codegen
Fetching latest versions of selected plugins...

    Config file generated at codegen.ts

      $ npm install

    To install the plugins.

      $ npm run codegen

    To run GraphQL Code Generator.
server/package.json
+  "scripts": {
+    "codegen": "graphql-codegen --config codegen.ts",
+    "compile": "npm run codegen && tsc",
+    "start": "npm run compile && node ./dist/src/index.js"
+  },

次に、 index.ts の resolvers の内容を別ファイルに切り出します。

MongoDB に対しての ORM として、今回は mongoose を利用して Book スキーマのモデルを作成します。

src/models/book.ts
import mongoose from "mongoose";

const bookSchema = new mongoose.Schema({
  title: String,
  author: String,
});

export const Book = mongoose.model("Book", bookSchema);

続いて、 server/src/graphql/resolvers.ts に GraphQL の Resolver を作ります。

server/src/graphql/resolvers.ts
import { AddBookMutationResponse } from "generated/graphql.js";
import { Book } from "../models/book.js";

export const resolvers = {
  Query: {
    books: async () => await Book.find(),
  },
  Mutation: {
    addBook: async (_, { title, author }): Promise<AddBookMutationResponse> => {
      const book = new Book({ title, author });
      try {
        await book.save();
        return {
          createdAt: new Date().toISOString(),
          message: "added",
          success: true,
          book: book,
        };
      } catch (error) {
        return {
          createdAt: null,
          message: "failed",
          success: false,
          book: null,
        };
      }
    },
  },
};

この状態で、 一旦 MongoDB をバックエンドとして、 GraphQL を叩くための環境を docker compose で確認できるようにします。

今回使用する Dockerfile は適当なので、 docker compose up --build を多用しますのであしからず。

server/Dockerfile
FROM node:22.3-slim
WORKDIR /apollo-server
COPY package*.json ./
RUN npm ci
COPY . .
CMD [ "npm", "run", "start" ]

また、DB に対する環境変数などを server/.env として定義しておきます。

server/.env
MONGO_URL="mongodb://mongodb:27017"
MONGO_INITDB_DATABASE="mydatabase"
MONGO_USER="root"
MONGO_PASS="root_password"
docker-compose.yml
services:
  mongodb:
    image: mongo:latest
    ports:
      - 27017:27017
    restart: unless-stopped
    container_name: mongodb
    volumes:
      - ./mongodb/data:/data/db
    env_file:
      - path: ./server/.env
  apolloserver:
    build:
      context: ./server
    depends_on:
      - mongodb

さらに Apollo Server から MongoDB に接続できるように server/index.ts を変更します。
dotenv を使用して環境変数読み込み、mongoose 使ってデータソースとして接続します。

server/src/index.ts
import { ApolloServer } from "@apollo/server";
import { startStandaloneServer } from "@apollo/server/standalone";
import { resolvers } from "./graphql/resolvers.js";
import { readFileSync } from "node:fs";
import mongoose from "mongoose";
import "dotenv/config";

const MONGO_URL = process.env.MONGO_URL as string;
const MONGO_INITDB_DATABASE = process.env.MONGO_INITDB_DATABASE as string;
const MONGO_USER = process.env.MONGO_USER as string;
const MONGO_PASS = process.env.MONGO_PASS as string;
const typeDefs = readFileSync("./schema.graphql", { encoding: "utf-8" });

const connection = await mongoose.connect(
  MONGO_URL,
  {
    user: MONGO_USER,
    pass: MONGO_PASS,
    dbName: MONGO_INITDB_DATABASE,
  }
);
console.log("🚀  Server ready at MongoDB");

const server = new ApolloServer({
  typeDefs,
  resolvers,
});

const { url } = await startStandaloneServer(server, {
  listen: { port: 4000 },
});

console.log(`🚀  Server ready at: ${url}`);

ひとまずこれで、 docker compose up で立ち上げて、 Apollo Server が用意してくれている GraphQL playground から GraphQL を実行・確認できるようになります。

下記は GraphQL playground の動作している様子です(books query 実行後の結果にあるデータは addBook の mutation で適当に突っ込んだものです)。

graphql playground

確認したら一旦 container を落として次に進みます。

Apollo Server と PostgreSQL の環境を作る

続いて、「書店」に関する Shop スキーマを定義して、PostgreSQL をデータソースとして接続します。
「本」のときと同様に Create と Read ができるようにして、さらに「本を在庫として書店に追加する」mutation と「書店から本の在庫を取得する」query を作ります。

server/schema.graphql
+ type Shop {
+   name: String
+   stock: [Book]
+ }
+
+ type AddShopMutationResponse {
+   createdAt: String!
+   success: Boolean!
+   message: String!
+   shop: Shop
+ }
+
+ type AddBookStockToShopMutationResponse {
+   createdAt: String!
+   success: Boolean!
+   message: String!
+   book: Book
+   shop: Shop
+ }
+
+ input BookToShopInput {
+   bookName: String
+   shopName: String
+ }
+
type Mutation {
  addBook(title: String, author: String): AddBookMutationResponse
+   addShop(name: String): AddShopMutationResponse
+   addBookToShop(input: BookToShopInput): AddBookStockToShopMutationResponse
}
+
type Query {
  books: [Book]
+   shops: [Shop]
+   getStock(name: String): [Book]
}

また、今回は PostgreSQL のデータモデルがすでに存在しているという状態を想定して init.sql を作ります。

postgresql/docker-entrypoint-initdb.d/init.sql
-- 店情報
CREATE TABLE shop (
    id SERIAL PRIMARY KEY,
    name VARCHAR(30) NOT NULL
);

-- 店情報とリレーションのある本の在庫情報
CREATE TABLE rel_shop_book (
    shopid INTEGER NOT NULL,
    bookid VARCHAR(24) NOT NULL,
    PRIMARY KEY (shopid, bookid),
    CONSTRAINT fk_shop
        FOREIGN KEY (shopid)
        REFERENCES shop(id)
        ON DELETE CASCADE
);
CREATE INDEX idx_rel_shop_book_shopid ON rel_shop_book(shopid);

PostgreSQL のための情報も env ファイルに追記します。このとき一旦接続先を localhost にしておきます(理由は後述)。

server/.env
+ POSTGRES_USER="testadmin"
+ POSTGRES_PASSWORD="testadmin"
+ POSTGRES_URL="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/"
docker-compose.yml
services:
+  postgres:
+    image: postgres
+    restart: always
+    env_file:
+      - path: ./server/.env
+    volumes:
+      - ./postgresql/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d
+      - ./postgresql/data:/var/lib/postgresql/data
+    tty: true
+    ports:
+      - 5432:5432

続いて、PostgreSQL への ORM としてPrisma を導入します。

Prisma が使えるように Apollo Server の Dockerfile をちょこっと変えています。

server/Dockerfile
+ # Install necessary libraries for Prisma
+ RUN apt-get update && \
+     apt-get install -y openssl libssl-dev zlib1g libgcc1 libc6 && \
+     apt-get clean && \
+     rm -rf /var/lib/apt/lists/*

npm install -D prisma でインストールを行い、npx prisma init で初期化しようとしたのですが、OrbStack の環境だと下記のエラーが発生したので、指示通り schema.prisma を修正しました。

apolloserver-1  | PrismaClientInitializationError: Prisma Client could not locate the Query Engine for runtime "linux-arm64-openssl-3.0.x".
apolloserver-1  |
apolloserver-1  | This happened because Prisma Client was generated for "darwin-arm64", but the actual deployment required "linux-arm64-openssl-3.0.x".
apolloserver-1  | Add "linux-arm64-openssl-3.0.x" to `binaryTargets` in the "schema.prisma" file and run `prisma generate` after saving it:
apolloserver-1  |
apolloserver-1  | generator client {
apolloserver-1  |   provider      = "prisma-client-js"
apolloserver-1  |   binaryTargets = ["native", "linux-arm64-openssl-3.0.x"]
apolloserver-1  | }
server/prisma/schema.prisma
+ generator client {
+   provider      = "prisma-client-js"
+   binaryTargets = ["native", "linux-arm64-openssl-3.0.x"]
+ }

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

この状態で一旦 docker compose up --buiid して PostgreSQL の container を立ち上げておき、npx prisma db pull で PostgreSQL のスキーマ情報から Prisma モデルをインポートしてスキーマを作ります(公式)。

このとき、datasource の url が localhost となるように server/.env を設定しましたが、prisma db pull ができるように localhost にしていました。

server/prisma/schema.prisma
+ /// The underlying table does not contain a valid unique identifier and can therefore currently not be handled by Prisma Client.
+ model rel_shop_book {
+   shopid Int
+   bookid String @db.VarChar(24)
+   shop   shop   @relation(fields: [shopid], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "fk_shop")
+
+   @@id([shopid, bookid])
+   @@index([shopid], map: "idx_rel_shop_book_shopid")
+ }

+  model shop {
+   id            Int             @id @default(autoincrement())
+   name          String          @db.VarChar(30)
+   rel_shop_book rel_shop_book[]
+ }

PostgreSQL のスキーマ情報が schema.prisma にインポートされたら container を落とします。

続いて Apollo Server で Prisma を使って開発するために、npx prisma generate で Prisma Client のコードを生成します(詳細はこちら)。

npm run codegen を実行して、schema.graphql に追加した Shop のスキーマのタイプを生成したら、いよいよ Prisma 用の Resolver を作っていきます。

server/src/graphql/resolvers.ts
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
export const resolvers = {
  Query: {
    // 一旦面倒なので、shopsはstockを返すように作っていません、、、
+     shops: async () => await prisma.shop.findMany(),
+     getStock: async (_, { name }): Promise<Books> => {
+       const shopWithBooks = await prisma.shop.findFirst({
+         where: {
+           name: name,
+         },
+         select: {
+           id: true,
+           rel_shop_book: {
+             select: {
+               bookid: true,
+             },
+           },
+         },
+       });
+
+       const bookObjectIds = shopWithBooks.rel_shop_book.map(
+         (book) => book.bookid
+       );
+
+       return Book.find({
+         _id: {
+           $in: bookObjectIds,
+         },
+       });
+     }
  },
  Mutation: {
+     addShop: async (_, { name }): Promise<AddShopMutationResponse> => {
+       const shop = await prisma.shop.create({
+         data: {
+           name: name,
+         },
+       });
+       return {
+         createdAt: new Date().toISOString(),
+         message: "added",
+         success: true,
+         shop: shop,
+       };
+     },
+     addBookToShop: async (
+       _,
+       {input}: MutationAddBookToShopArgs
+     ): Promise<AddBookStockToShopMutationResponse> => {
+       const isExistBook = await Book.findOne({ title: input.bookName });
+       const isExistShop = await prisma.shop.findFirst({
+         where: {
+           name: input.shopName,
+         },
+       });
+       if (isExistBook !== null && isExistShop !== null) {
+         const res = await prisma.rel_shop_book.create({
+           data: {
+             bookid: String(isExistBook._id),
+             shopid: isExistShop.id,
+           },
+         });
+         return {
+           createdAt: new Date().toISOString(),
+           message: "added",
+           success: true,
+           book: isExistBook,
+           shop: isExistShop,
+         };
+       }
+     },
  },
};

Apollo Server と Prisma を使って PostgreSQL が接続できるようになったので、動作確認を GraphQL playground から行うのですが、prisma db pull のために POSTGRES_URLlocalhost に変えていたので server/.env を修正します。

server/.env
- POSTGRES_URL="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/"
+ POSTGRES_URL="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/"

docker compose up --build して GraphQL playground から動作確認を行います。

まずは BookShop1 を登録します。

shop 登録

続いてこの BookShop1 に MongoDB にて登録した本 hogehoge を追加します。

shopにbookを登録

最後に、BookShop1hogehoge が登録されているか、getStock で確認します。

getStockで確認

これで Apollo Server に MongoDB と PostgreSQL 複数のデータソースを利用できる環境が完成しました。

実際の Web アプリの構築

ここまで来たら web-client 側を作り込み、Apollo Server へ GraphQL のリクエストを試せる環境を作ります。

適当にさっさと組みたいので、今回は Vite + React の構成です(https://vitejs.dev/guide/#getting-started)

npm create vite@latest web-client -- --template react-ts

web-client で使う GraphQL の 型を簡単生成する準備

稼働中の Apollo Server から型や定義などを取得して Apollo Client で開発できるようにします。

公式に従えば基本的に問題ないです。

下記必要な package をインストールします。

npm i -D typescript graphql @graphql-codegen/cli @graphql-codegen/client-preset @graphql-typed-document-node/core

続いて、 docker compose up --build して Apollo Server を立ち上げておき codegen.ts を作ります。

Apollo Server は localhost:4000 で待ち受けているので、schema を Apollo Server に設定しておきます。

web-client/codegen.ts
const config: CodegenConfig = {
  schema: "http://localhost:4000/",
  // this assumes that all your source files are in a top-level `src/` directory - you might need to adjust this to your file structure
  documents: ["src/**/*.{ts,tsx}"],
  generates: {
    "./src/__generated__/": {
      preset: "client",
      plugins: [],
      presetConfig: {
        gqlTagName: "gql",
      },
    },
  },
  ignoreNoDocuments: true,
};

export default config;
web-client/package.json
{
  "scripts": {
+    "compile": "graphql-codegen",
+    "watch": "graphql-codegen -w"
  },
}

あとは graphql-codegen を実行すれば(npm run compile)、 codegen.ts で設定したディレクトリに GraphQL の型が生成されているので、これを使って web-client 側で作り込みます。

Apollo Client を利用してサーバー側に接続してコンポーネントを作り込む

Apollo Client を利用して web アプリから稼働中の Apollo Server へ接続し、 UI とかを作って GraphQL のリクエストを試します。

Apollo Client は、web-client のコンポーネントツリー全体で接続情報を使用できるように ApolloProvider を提供していて、これを React App に wrap すればコンポーネントのどこからでも接続して利用できます。(参考)

今回はお試しなので、 React App 全体に ApolloProvider を wrap します。

web-client/src/main.tsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import './index.css'
+ import { ApolloClient, ApolloProvider, InMemoryCache } from '@apollo/client';

+ const client = new ApolloClient({
+   uri: "http://localhost:4000/graphql",
+   cache: new InMemoryCache(),
+ });

ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
+     <ApolloProvider client={client}>
        <App />
+     </ApolloProvider>
  </React.StrictMode>
);

続いて、前節で GraphQL の型を簡単に生成できる準備しましたが、React コンポーネントを作っていく際にどのように生成された型を利用して、開発を進めていくか地道にやってみたいと思います。

まずは適当なコンポーネント名でファイルを作って、最初自動生成した型からそのコンポーネントが取得したい情報の query を先に書きます。

例えば、 GetAllBooks.tsx というコンポーネントだったら、下記のように books を使って GraphQL ドキュメントを書けば、欲しい情報が取得できそうだなと判断できます(下記は自動生成された Query の型)。

web-client/src/__generated__/graphql.ts
export type Book = {
  __typename?: 'Book';
  author?: Maybe<Scalars['String']['output']>;
  title?: Maybe<Scalars['String']['output']>;
};

export type Query = {
  __typename?: 'Query';
  books?: Maybe<Array<Maybe<Book>>>;
  getStock?: Maybe<Array<Maybe<Book>>>;
  shops?: Maybe<Array<Maybe<Shop>>>;
};

上記から GraphQL ドキュメントを一旦コンポーネントに書き出しておいて、

web-client/src/components/GetAllBooks.tsx
+ import { gql } from "../__generated__";
+
+ const allBookQuery = gql(`
+  query allbooks {
+   books {
+    title
+    author
+   }
+  }
+ `);

この状態で再度 npm run compile(graphql-codegen) すると、web-client 側で開発に使用するための GraphQL ドキュメントの型が自動生成されます。

web-client/src/__generated__/graphql.ts
+ export type AllbooksQueryVariables = Exact<{ [key: string]: never; }>;
+ export type AllbooksQuery = { __typename?: 'Query', books?: Array<{ __typename?: 'Book', title?: string | null, author?: string | null } | null> | null };
+ export const AllbooksDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"allbooks"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"books"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"author"}}]}}]}}]} as unknown as DocumentNode<AllbooksQuery, AllbooksQueryVariables>;

このように、欲しい情報を書いたら compile して GraphQL ドキュメントの型をどんどん生成しながらコンポーネントを作り込んでいきます。

watch モードを使えばどんどん生成できるので詳細は公式を参照いただければと思いますが、今回は泥臭くやっていきます。

GraphQL ドキュメントの型が生成されたので、これを使ってコンポーネントを完成させます(typescript-react-apollo のプラグインもあるのでこちらを利用したほうがスッキリします)。

web-client/src/components/GetAllBooks.tsx
+ import { useLazyQuery } from "@apollo/client";
+ import {
+   AllbooksDocument,
+   AllbooksQuery,
+ } from "../__generated__/graphql";
+ export function GetAllBooks() {
+   const [books, { loading, error, data }] =
+     useLazyQuery<AllbooksQuery>(AllbooksDocument);
+
+   return (
+     <div className="max-w-md mx-auto p-4">
+       <h1 className="text-2xl font-bold mb-4">Book Search</h1>
+       <div className="mb-4">
+         <button
+           onClick={() => books()}
+           className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
+         >
+           本一覧
+         </button>
+       </div>
+       <ul className="list-disc pl-5">
+         {!loading && !error && data?.books && data.books.length > 0 ? (
+           data.books.map((book) => {
+             if (book !== null) {
+               return <li
+                 key={book.title}
+               >{`タイトル: ${book.title} (著者: ${book.author})`}</li>
+             }
+           })
+         ) : (
+           <li>本一覧はまだありません。</li>
+         )}
+       </ul>
+     </div>
+   );
+ }

本一覧を取得するだけのボタンが付いた適当な UI ですが、動けばなんでもいいです。

npm run dev でささっと動作確認します。

本を取得するボタンだけ

ボタンを押したらちゃんと本一覧が取得できていることが確認できます。

ボタンを押したらMongoDBに保存されている本が

これ以降はコードを省略しますが、「書店の検索」や「本を書店に追加する」ような UI も作ります。

とりあえず完成した画面

現在 MongoDB と PostgreSQL に登録されている情報が取得できることを確認します。

本一覧と書店の在庫を確認

書籍名が test の本を店舗名 BookShop1 に追加してみます。

追加のボタンを押すと、メッセージである added が表示されます。

正しく本が登録されているメッセージ

最後に、改めて BookShop1 で在庫検索すると、下記の通り見事に本が追加されていることが確認できました。

登録されていた!

以上で web アプリ自体の動作確認も完了できたので、最後に web-client も docker compose で動くようにいい感じにしていきます。

このままだと npm run build したときに、コンポーネントに定義していた使用する GraphQL ドキュメントが unused variable で error になってしまうので、これを schema として別出しします。

具体的には、それぞれのコンポーネントの gql で定義しているものを .graphql として定義し直します。

web-client/src/components/GetAllBooks.tsx
- import { gql } from "../__generated__";
-
- const allBookQuery = gql(`
-  query allbooks {
-   books {
-    title
-    author
-   }
-  }
- `);
web-client/src/libs/graphql/queries/allbooks.graphql
query allbooks {
  books {
    title
    author
  }
}
src/libs/
└── graphql
    ├── mutations
    │   └── addBookToShop.graphql
    └── queries
        ├── allbooks.graphql
        └── getStock.graphql

そして、codegen.ts についても graphql の拡張子でも生成できるように設定します。

web-client/codegen.ts
const config: CodegenConfig = {
  schema: "http://localhost:4000/",
-  documents: ["src/**/*.{ts,tsx}"],
+  documents: ["src/**/*.{ts,tsx,graphql}"],
  generates: {

  },
  ignoreNoDocuments: true,
};

最後に web-client の Dockerfile を作って、 ApolloProvider の接続先も Apollo Server のコンテナ名にして完了です。

web-client/Dockerfile
FROM node:22.3-slim

WORKDIR /app

COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
CMD [ "npm", "run", "preview"]
docker-compose.yml
+   web-client:
+     build:
+       context: ./web-client
+     depends_on:
+       - apolloserver
+     ports:
+       - 8080:8080

本番では https 化や CORS への対応などが発生しますが、今回はお試し開発環境なので vite proxy を設定して同一オリジンとして接続します。(公式のHTTPS対応CORSへの対応)

web-client/vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  server: {
    host: true,
+     proxy: {
+       "/graphql": {
+         target: "http://apolloserver:4000",
+         changeOrigin: true,
+         secure: false,
+       },
+     },
  },
});
main.tsx
const client = new ApolloClient({
-  uri: "http://apolloserver:4000/graphql",
+  uri: "/graphql",
  cache: new InMemoryCache(),
});

お疲れ様でした。これで docker compose up だけで、MongoDB・PostgreSQL・GraphQL Server・Web アプリを試す環境を作ることができました。

最後に

今回は、GraphQL の練習のために MongoDB + PostgreSQL の DB Apollo Server の環境を構築して Web アプリを作って動作確認までできました。
まだまだ GraphQL は修行中の身ですが、今回のような練習できる環境を立ち上げることで、GraphQL によって異なるデータベースを扱える方法について理解を深めることができました。

今後の展望としては、今回の基礎環境をさらに発展させ、より高度な実践に近い機能を持つ Web アプリを題材としてやっていけたら楽しいかなと思っています。具体的には subscription とか CORS とかの機能とかを試したり N+1 問題とかに対応してみたりしたいですね。

参考文献

Discussion