LoginSignup
3
1

[GraphQL学習支援] Nuxt3 + Apollo Server で学習用お手軽GraphQLサーバ(とクエリ発行用の砂場)を作る

Last updated at Posted at 2022-11-21

[GraphQL学習支援] Nuxt3 + Apollo Server で学習用お手軽GraphQLサーバ(とクエリ発行用の砂場)を作る

先にコードだけ教えて

これだよ

上記リポジトリをクローンして npm install & npm run dev すれば、モックとして実装したGraphQLサーバと、クエリを発行できる砂場を起動できるよ。

(前者が http://localhost:3000/api/graphql で、後者が http://localhost:3000/ で起動する)

graphql_nuxt3_sandbox_04.gif

経緯

  • GraphQLの学習をしたい時に、ちょっと手元でいじれるGraphQLサーバーがあったらいいなと思ったので、
  • git cloneでローカルに落とせてお手軽に試せるGraphQLサーバーを Nuxt3 + Apollo Server で作ることにしたよ
  • 当初Nuxt3ではなくNitroでやろうとしたから、その時の記録も残っているよ
Nitro + Apollo Server 版はこちら(ちょっと挙動に怪しい部分があるので、あくまで参考までに)

NitroでGraphQLサーバーを構築してみた記録 (注:ちょっと挙動が怪しい)

Nitroというのはつい先日正式版がリリースされたNuxt3のサーバエンジンとして利用されているWebフレームワークで、その内部では H3 という高性能と移植性が売りのフレームワークに依存しているよ。

Apollo Server の公式ドキュメントで、特定のWebフレームワークにミドルウェアとして組み込む場合にちゃんと動くようコミュニティが(現時点で)メンテナンスしているパッケージの一覧があるのだけど、この中に H3 が含まれているから {{{{ 多分だけど(重要) }}}} Nitroでも動くと思うよ。

https://www.apollographql.com/docs/apollo-server/integrations/integration-index

そういうわけで早速実装してみよう。

適当なディレクトリに入って、下記のコマンドを実行してね。

mkdir otameshi_graphql
cd otameshi_graphql
mkdir server
cd server

# otameshi_graphql/serverディレクトリをNitroサーバのルートディレクトリとして構築する
npm install --save-dev nitropack
echo '{
  "extends": "./.nitro/types/tsconfig.json",
  "compilerOptions": {
    "strict": true,
  }
}' > tsconfig.json

# Nitroサーバを仮実装
mkdir routes
echo 'export default eventHandler(e => "Hi!");' > routes/graphql.ts

# ここまで実行したら、 `otameshi_graphql/server` でVSCodeを起動
# (自分の好きなエディタに読み替えてね)
code .

VSCodeを開いたら、package.jsonに次のscriptsを追加しよう。

 {
+  "scripts": {
+    "nitropack": "nitropack",
+    "dev": "nitropack dev",
+    "build": "nitropack build"
+  },
   "devDependencies": {
     "nitropack": "^1.0.0"
   }
 }

ここまで出来たら、まずは仮実装のNitroサーバが動くか試してみるよ ( .nitro ディレクトリの自動生成をフックするためでもある )

# yarn派なら yarn dev に読み替えてね
npm run dev

サーバが立ち上がったら、 http://localhost:3000/graphql をcurl等でコールしてみて。
(もちろんブラウザでアクセスするでも良し)

curl http://localhost:3000/graphql

# responseとして Hi! が返ってくるはず

Nitroで作った雛形サーバにGraphQLを組み込む

まずは必要なパッケージをインストールしないとね。H3 と Apollo Server の統合用パッケージ @as-integrations/h3 の npm 上のドキュメントに従って必要なパッケージをインストールしようか。

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

そういうわけで、serverディレクトリ直下で下記のコマンドを実行するよ。

npm install --save-dev @apollo/server graphql @as-integrations/h3

実行したら、さっき仮実装した routes/graphql.ts を次のように編集しようか。

routes/graphql.ts
import { ApolloServer } from "@apollo/server";
import { startServerAndCreateH3Handler } from "@as-integrations/h3";

const typeDefs = `#graphql
  type Query {
    hello: String
  }
`;
const resolvers = {
  Query: {
    hello: () => "world",
  },
};
const apollo = new ApolloServer({
  typeDefs,
  resolvers,
});

export default startServerAndCreateH3Handler(apollo);

さて、これで簡単なGraphQLサーバを実装できたね。さっそく npm run dev で起動してみると……

スクリーンショット 2022-11-18 13.06.05.jpg

うん、動かないね。

と思ったけど、諦めずに一度開発サーバを停止させて、もう一度 npm run dev で動かすと、なぜかエラーなく動いたよ。

試しに curl で作成したGraphQLエンドポイントにクエリを投げてみると、ちゃんと応答が返ってくることが確認できた。

% curl -X POST -H "Content-Type: application/json" \
  --data '{"query":"{ hello }"}' \
  http://localhost:3000/graphql
{"data":{"hello":"world"}}
% 

そういうわけだから、Nitroはちょっと挙動の怪しいところがあるけれど、最低限 Apollo Server と組み合わせて開発サーバーを立ち上げるところまでは確認できたよ。

興味のある人はOSS開発に加わって、怪しい挙動なく動くように出来たらきっと喜ぶ人がいると思うよ。

最後に一応、今回の Nitro + Apollo Server の組み合わせのパッケージのバージョンを記載しておくね。

"@apollo/server": "^4.1.1",
"@as-integrations/h3": "^1.1.3",
"graphql": "^16.6.0",
"nitropack": "^1.0.0"

筆者プロフィール

Kenpal株式会社でITエンジニアとして色々いじってる faable01 です。

ものづくりが好きで、学生時代から創作仲間と小説を書いたりして楽しんでいたのですが、当時はその後自分がIT技術者になるとはつゆ程も思っていませんでした。紆余曲折あり、20代の半ばを過ぎた頃に初めてこの業界と出会った形です。

趣味は「技術記事を口語で書くこと」です。
個人サイトでもちょくちょく技術記事を書いてます。読んでね(これを追記した2023年4月時点では、よくAWS CDKをラップした爆速サーバレス開発ツールのSSTについて記事にしてるよ)

それから、業務日報SaaS 「RevisNote」 を運営しています。リッチテキストでの日報と、短文SNS感のある分報を書けるのが特徴で、組織に所属する人数での従量課金制です。アカウント開設後すぐ使えて、無料プランもあるから、気軽にお試しください。

Nuxt3 + Apollo Server で GraphQLサーバを構築する

さっそく、Nuxt3とApollo ServerでGraphQLサーバを構築していこう。

適当なディレクトリ配下に otameshi_graphql/nuxt3 ディレクトリを作成して、その中に Nuxt3 + Apollo Server を構築するよ。

まずは Nuxt3 の雛形プロジェクトを作成しようか。

mkdir otameshi_graphql
cd otameshi_graphql
npx nuxi init nuxt3

nuxi init が Nuxt3 の雛形作成コマンドだね。

雛形の作成が完了したら、次はnuxt3ディレクトリに入って必要なパッケージのインストールや、Server API Routes への雛形GraphQLルートの作成を行うよ。

cd nuxt3
npm install --save-dev @apollo/server graphql @as-integrations/h3
mkdir server
mkdir server/api
echo 'import { ApolloServer } from "@apollo/server";
import { startServerAndCreateH3Handler } from "@as-integrations/h3";

const typeDefs = `#graphql
  type Query {
    hello: String
  }
`;
const resolvers = {
  Query: {
    hello: () => "world",
  },
};
const apollo = new ApolloServer({
  typeDefs,
  resolvers,
});

export default startServerAndCreateH3Handler(apollo);' > server/api/graphql.ts
npm run dev

開発サーバーが立ち上がったら、curlコマンドでGraphQLサーバからちゃんと期待通りの応答が返ってくるか試してみようか。

curl -X POST -H "Content-Type: application/json" \
  --data '{"query":"{ hello }"}' http://localhost:3000/api/graphql

# ▼ 得られるレスポンス
# {"data":{"hello":"world"}}

無事、GraphQLサーバとして動くことが確認できたね。

ただ、今のままだとちょっと寂しいから、もうちょっとGraphQLサーバーの内容を充実させてみようか。

スキーマを増やして、もうちょっと「それっぽい」GraphQLサーバをNuxt3上に構成してみる

あくまで砂場として使えるGraphQLサーバを作りたいから、あえてDBには繋げないよ。Nuxt3のサーバ内で、単なるオブジェクトとして保持するユーザー情報を追加したり編集できるGraphQLサーバを作ろうね。

まずは前準備として、とりあえずuuid生成用のライブラリを入れるよ。

npm install --save-dev uuid @types/uuid

それから、クエリ発行画面を作りたいから、その時のために Tailwind CSS を導入しておくよ。

npm install --save-dev @nuxtjs/tailwindcss

さらにNuxt3の設定ファイルを次のように設定するよ。

nuxt.config.ts
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
  // Nuxt3上のTypeScriptの型チェックを厳密にする
  typescript: {
    strict: true,
  },
  // Tailwind CSS をモジュールとして導入
  modules: ["@nuxtjs/tailwindcss"],
});

前準備はこれだけ。

ここからはGraphQLサーバの構築作業だよ。

最初にスキーマを別ファイルで定義しよう。Nuxt3アプリケーションのルートディレクトリ直下に typeDefs.ts を作成して、その中でスキーマを書こうか。

ユーザの名前と年齢を持つエンティティに対し、CRUD操作をできるQueryとMutationを定義してくよ。

typeDefs.ts
export const typeDefs = `#graphql
  type User {
    # uuid
    id: ID
    # フルネーム
    full_name: String
    # 年齢
    age: Int
  }
  input UserCreateInput {
    # フルネーム
    full_name: String
    # 年齢
    age: Int
  }
  input UserUpdateInput {
    # uuid
    id: ID
    # フルネーム
    full_name: String
    # 年齢
    age: Int
  }
  input UserDeleteInput {
    # uuid
    id: ID
  }
  input UserQueryInput {
    # uuidを直接指定できる引数
    id: ID
    # 「名前の一部分」を指定できる引数
    name_contains: String
    # 年齢の範囲を指定できる引数(最小値)
    age_gte: Int
    # 年齢の範囲を指定できる引数(最大値)
    age_lte: Int
  }
  type Query {
    # ユーザーを取得する
    users(input: UserQueryInput): [User]
  }
  type Mutation {
    # ユーザーを作成する
    createUser(input: UserCreateInput): User
    # ユーザーを更新する
    updateUser(input: UserUpdateInput): User
    # ユーザーを削除する
    deleteUser(input: UserDeleteInput): User
  }
`;

こんなところだね。

そうしたら、このtypeDefsを使って server/api/graphql.ts にGraphQLサーバを構築しようか。

(実務レベルのGraphQLサーバ開発なら、スキーマからTypeScriptの型を生成したりするけれど、今回は学習用の小規模なものだから、あえてそういったことをしていないよ)

server/api/graphql.ts
import { ApolloServer } from "@apollo/server";
import { startServerAndCreateH3Handler } from "@as-integrations/h3";
import { v4 as uuidv4 } from "uuid";
import { typeDefs } from "~~/typeDefs";

type User = {
  id: string;
  full_name: string;
  age: number;
};
type UserCreateInput = {
  full_name: string;
  age: number;
};
type UserUpdateInput = {
  id: string;
  full_name: string;
  age: number;
};
type UserDeleteInput = {
  id: string;
};
type UserQueryInput = {
  id?: string;
  name_contains?: string;
  age_gte?: number;
  age_lte?: number;
};

const users: User[] = [
  ...Array(10)
    .fill(null)
    .map((_, i) => ({
      // 動作確認用に1つだけuuidを固定させている
      id: i === 0 ? "660688c2-3b47-4de3-ad0e-af92a75731f9" : uuidv4(),
      full_name: `${
        ["John", "Mike", "Tom", "Bob"][
          // ランダムに選ぶ
          Math.floor(Math.random() * 4)
        ]
      } ${
        ["Smith", "Brown", "Johnson", "Williams"][
          // ランダムに選ぶ
          Math.floor(Math.random() * 4)
        ]
      }`,
      age: 20 + i,
    })),
];

const resolvers = {
  Query: {
    users: (_: unknown, { input }: { input?: UserQueryInput }) => {
      return users.filter((user) => {
        return (
          !input ||
          ((!input.id || user.id === input.id) &&
            (!input.name_contains ||
              user.full_name.includes(input.name_contains)) &&
            (!input.age_gte || user.age >= input.age_gte) &&
            (!input.age_lte || user.age <= input.age_lte))
        );
      });
    },
  },
  Mutation: {
    createUser: (_: unknown, { input }: { input: UserCreateInput }) => {
      const user = {
        id: uuidv4(),
        full_name: input.full_name,
        age: input.age,
      };
      users.push(user);
      return user;
    },
    updateUser: (_: unknown, { input }: { input: UserUpdateInput }) => {
      const user = users.find((user) => user.id === input.id);
      if (!user) throw new Error("User not found");
      user.full_name = input.full_name;
      user.age = input.age;
      return user;
    },
    deleteUser: (_: unknown, { input }: { input: UserDeleteInput }) => {
      const user = users.find((user) => user.id === input.id);
      if (!user) throw new Error("User not found");
      users.splice(users.indexOf(user), 1);
      return user;
    },
  },
};
const apollo = new ApolloServer({
  typeDefs,
  resolvers,
});

/**
 * ## 各クエリのcurl実行例
 *
 * ```
 * curl -X POST -H "Content-Type: application/json" -d '{"query":"{ users(input: {}) { id full_name age } }"}' http://localhost:3000/api/graphql
 * curl -X POST -H "Content-Type: application/json" -d '{"query":"{ users(input: { id: \"660688c2-3b47-4de3-ad0e-af92a75731f9\" }) { id full_name age } }"}' http://localhost:3000/api/graphql
 * curl -X POST -H "Content-Type: application/json" -d '{"query":"{ users(input: { name_contains: \"J\" }) { id full_name age } }"}' http://localhost:3000/api/graphql
 * curl -X POST -H "Content-Type: application/json" -d '{"query":"{ users(input: { age_gte: 25, age_lte: 30 }) { id full_name age } }"}' http://localhost:3000/api/graphql
 * curl -X POST -H "Content-Type: application/json" -d '{"query":"mutation { createUser(input: { full_name: \"John Smith\", age: 20 }) { id full_name age } }"}' http://localhost:3000/api/graphql
 * curl -X POST -H "Content-Type: application/json" -d '{"query":"mutation { updateUser(input: { id: \"660688c2-3b47-4de3-ad0e-af92a75731f9\", full_name: \"John Smith\", age: 20 }) { id full_name age } }"}' http://localhost:3000/api/graphql
 * curl -X POST -H "Content-Type: application/json" -d '{"query":"mutation { deleteUser(input: { id: \"660688c2-3b47-4de3-ad0e-af92a75731f9\" }) { id full_name age } }"}' http://localhost:3000/api/graphql
 * ```
 */
export default startServerAndCreateH3Handler(apollo);

これでスキーマ定義に従い、ユーザのCRUD操作を行うQueryとMutationを実装出来たよ。

GraphQLサーバの実装としてはこれでおしまい。

ソースコードのコメント中にも書いたけど、curlコマンドで動作確認するなら、次のようなコマンドで確認できるよ。

  • 全ユーザ取得Query:
    curl -X POST -H "Content-Type: application/json" -d '{"query":"{ users(input: {}) { id full_name age } }"}' http://localhost:3000/api/graphql
    
  • ID指定Query:
    curl -X POST -H "Content-Type: application/json" -d '{"query":"{ users(input: { id: \"660688c2-3b47-4de3-ad0e-af92a75731f9\" }) { id full_name age } }"}' http://localhost:3000/api/graphql
    
  • 名前の部分一致Query:
    curl -X POST -H "Content-Type: application/json" -d '{"query":"{ users(input: { name_contains: \"J\" }) { id full_name age } }"}' http://localhost:3000/api/graphql
    
  • 年齢の範囲指定Query:
    curl -X POST -H "Content-Type: application/json" -d '{"query":"{ users(input: { age_gte: 25, age_lte: 30 }) { id full_name age } }"}' http://localhost:3000/api/graphql
    
  • ユーザ作成Mutation:
    curl -X POST -H "Content-Type: application/json" -d '{"query":"mutation { createUser(input: { full_name: \"John Smith\", age: 20 }) { id full_name age } }"}' http://localhost:3000/api/graphql
    
  • ユーザ更新Mutation:
    curl -X POST -H "Content-Type: application/json" -d '{"query":"mutation { updateUser(input: { id: \"660688c2-3b47-4de3-ad0e-af92a75731f9\", full_name: \"John Smith\", age: 20 }) { id full_name age } }"}' http://localhost:3000/api/graphql
    
  • ユーザ削除Mutation:
    curl -X POST -H "Content-Type: application/json" -d '{"query":"mutation { deleteUser(input: { id: \"660688c2-3b47-4de3-ad0e-af92a75731f9\" }) { id full_name age } }"}' http://localhost:3000/api/graphql
    

これで npm run dev でlocalhostにGraphQLサーバを立ち上げられることができるようになったから、あとは自分が学習したいプログラミング言語やフレームワークからGraphQLサーバへのクエリを投げて、感触を掴んでみてね。

……という形でこの記事を終えるつもりだったけど、せっかくだから(あと学習しやすくしたいから)Nuxt3のフロントエンドで、ブラウザからGraphQLサーバへ自由にクエリを投げられるサンドボックス画面を作るよ。

Nuxt3のフロントエンドから、GraphQLサーバへのクエリ発行を分かりやすく試せる砂場を作ってみる

結論から言うと、次のような 「ブラウザ上のテキストエリアからクエリを入力してサーバに投げることのできる画面」 を作るよ。

graphql_nuxt3_sandbox_04.gif

走り書きで作るよ。Nuxt3のルートディレクトリにあるapp.vue単品で一気に書いていこう。(とりあえず短時間で動かすことを優先しているから、コードを綺麗にしようという意識ゼロのコードだよ。ごめんね)

app.vue
<script setup lang="ts">
import { ChangeEvent } from "rollup";
import { typeDefs } from "~~/typeDefs";

const query = ref(`query {users { id full_name age }}`);
const result = ref("ここに結果が表示されるよ");
const execute = async () => {
  const res = await fetch("/api/graphql", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      query: query.value,
    }),
  });
  result.value = JSON.stringify(await res.json(), null, 2);
};
const schema = typeDefs;
const showSchema = ref(false);

const sampleQueries = {
  全ユーザ取得Query: `query {
  users {
    id
    full_name
    age
  }
}`,
  ID指定Query: `query {
  users(input: { id: "660688c2-3b47-4de3-ad0e-af92a75731f9" }) {
    id
    full_name
    age
  }
}`,
  名前の部分一致Query: `query {
  users(input: { name_contains: "J" }) {
    id
    full_name
    age
  }
}`,
  年齢の範囲指定Query: `query {
  users(input: { age_gte: 25, age_lte: 30 }) {
    id
    full_name
    age
  }
}`,
  ユーザ作成Mutation: `mutation {
  createUser(input: { full_name: "John Smith", age: 20 }) {
    id
    full_name
    age
  }
}`,
  ユーザ更新Mutation: `mutation {
  updateUser(
    input: {
      id: "660688c2-3b47-4de3-ad0e-af92a75731f9"
      full_name: "John Smith"
      age: 20
    }
  ) {
    id
    full_name
    age
  }
}`,
  ユーザ削除Mutation: `mutation {
  deleteUser(input: { id: "660688c2-3b47-4de3-ad0e-af92a75731f9" }) {
    id
    full_name
    age
  }
}`,
}
</script>

<template>
  <div class="container mx-auto max-w-5xl">
    <div class="m-4 border-2 bg-emerald-100 p-4 grid gap-y-4">
      <h1 class="text-2xl font-bold text-gray-800">
        GraphQL Sandbox
      </h1>
      <h2 class="text-xl font-bold text-gray-800 border-l-8 border-l-emerald-400 pl-2">
        使い方
      </h2>
      <p>
        下のテキストエリアにGraphQLのクエリを書いて、実行ボタンを押すと、結果が表示されます。
      </p>
      <p>
        なおGraphQLサーバーのエンドポイントは Nuxt3 Server API Routes の
        <code class="text-sm font-bold text-gray-800">
          /api/graphql
        </code>
        です
      </p>
      <h2 class="text-xl font-bold text-gray-800 border-l-8 border-l-emerald-400 pl-2">
        クエリ
      </h2>
      <button
        class="text-sm font-bold text-gray-800 border-2 border-gray-800 rounded-md px-2 py-1 max-w-md bg-white hover:bg-gray-800 hover:text-white"
        @click="showSchema = true">
        スキーマを見る?
      </button>
      <teleport to="body">
        <div v-if="showSchema" @click="(event) => {
          // 子要素以外の場所をクリックしたら showSchema = false; にする
          if (event.target === event.currentTarget) {
            showSchema = false;
          }
        }" class="fixed inset-0 bg-black bg-opacity-50 flex justify-center items-center h-screen w-screen">
          <!-- 閉じるボタンをバツで表現する -->
          <button class="absolute top-0 right-0 m-6 text-5xl font-bold text-white hover:text-gray-200"
            @click="showSchema = false">
            ×
          </button>
          <div class="bg-white rounded-md p-4 w-4/5 h-4/5 overflow-auto">
            <pre class="text-sm font-mono text-gray-800">{{ schema }}</pre>
          </div>
        </div>
      </teleport>
      <!-- selectでsampleQueriesの中から一つ選び、queryに設定する -->
      <select
        class="text-sm font-bold text-gray-800 border-2 border-gray-800 rounded-md px-2 py-1 max-w-md hover:bg-gray-800 hover:text-white"
        @change="(e) => {
          query = (e.target as HTMLSelectElement | null)?.value ?? '';
        }">
        <option disabled selected>
          サンプルクエリを選択してください
        </option>
        <option v-for="(value, key) in sampleQueries" :value="value">{{ key }}</option>
      </select>
      <textarea v-model="query" rows="10" cols="100"
        class="border-2 border-gray-300 bg-gray-100 bg-opacity-70 rounded-md p-2 w-full">
      </textarea>
      <div>
        <button
          class="bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-600 focus:ring-opacity-50"
          @click="execute">
          実行
        </button>
      </div>
      <!-- 下記にコードの実行結果を表示する -->
      <h2 class="text-xl font-bold text-gray-800 border-l-8 border-l-emerald-400 pl-2">
        実行結果
      </h2>
      <div class="border-2 border-gray-300 bg-gray-100 bg-opacity-70 rounded-md p-2 w-full overflow-x-auto">
        <!-- スクロール可能なpreタグ -->
        <pre class="overflow-x-auto">{{ result }}</pre>
      </div>
    </div>
  </div>
</template>

こんな感じだね。

ここまで書けたら npm run dev で開発サーバを立ち上げて、 http://localhost:3000/ にアクセスしよう。

そうすると、さっきGIFアニメーションで見せたような画面が表示されるから、あとはテキストエリアから自由にクエリを発行できるよ。

あとは触ったり改変したりしながら、GraphQLのことを学習していってね。

まとめ

Nuxt3 + Apollo Server で GraphQL サーバの雛形を構築するなら、次のコマンドだけでいけるよ。

npx nuxi init [プロジェクト名]
cd [プロジェクト名]
npm install --save-dev @apollo/server graphql @as-integrations/h3
mkdir server
mkdir server/api
echo 'import { ApolloServer } from "@apollo/server";
import { startServerAndCreateH3Handler } from "@as-integrations/h3";

const typeDefs = `#graphql
  type Query {
    hello: String
  }
`;
const resolvers = {
  Query: {
    hello: () => "world",
  },
};
const apollo = new ApolloServer({
  typeDefs,
  resolvers,
});

export default startServerAndCreateH3Handler(apollo);' > server/api/graphql.ts
npm run dev

さらにここから色々肉付けしていって、

  • モックとして動作するGraphQLサーバと
  • クエリを発行する砂場の画面

を作るなら、次のリポジトリのようなコードでいけるよ。

GraphQLの学習をしたい人は、いじってみてね。

(サーバ側の学習は不要で、クエリを発行する側の実装だけ学習したい人も、モックサーバと砂場が役立つと思うよ。よかったら触ってみてね)

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1