実際にGraphQLサーバーを構築してみよう!

プロ太先生、前回GraphQLがすごく便利そうだって分かりました!
実際に作ってみたいです!

よし!今日は実際にGraphQLサーバーを作ってみよう。Node.jsとApollo Serverを使うから、まずは環境を準備しよう。

事前準備

まず、Node.jsがインストールされているか確認しよう。ターミナルで以下のコマンドを実行してみて。

node --version
npm --version

Node.jsは入ってます!バージョンも新しいものでした。

それじゃあ新しいプロジェクトを作成しよう。

# 新しいディレクトリを作成
mkdir my-first-graphql
cd my-first-graphql

# package.jsonを作成
npm init -y

# 必要なパッケージをインストール
npm install apollo-server-express express graphql
npm install --save-dev nodemon

最初のGraphQLサーバーを作成

それでは、まず簡単なGraphQLサーバーを作ってみよう。server.jsというファイルを作成して、以下のコードを書いてみて。

// server.js
const { ApolloServer, gql } = require('apollo-server-express');
const express = require('express');

// GraphQLのスキーマを定義(どんなデータがあるかを決める設計図)
const typeDefs = gql`
  # ユーザーという型を定義
  type User {
    id: ID!          # IDは必須項目(!マークで必須を表す)
    name: String!    # 名前は文字列で必須
    email: String!   # メールアドレスも文字列で必須
    age: Int         # 年齢は整数(必須ではない)
  }

  # クエリ(データを取得する処理)を定義
  type Query {
    # すべてのユーザーを取得する
    users: [User!]!  # User型の配列を返す、必須
    
    # 特定のIDのユーザーを取得する
    user(id: ID!): User  # IDを受け取って、Userを返す(見つからない場合はnull)
  }
`;

// サンプルデータ(本来はデータベースから取得)
const sampleUsers = [
  { id: '1', name: '山田太郎', email: 'yamada@example.com', age: 25 },
  { id: '2', name: '田中花子', email: 'tanaka@example.com', age: 30 },
  { id: '3', name: '佐藤次郎', email: 'sato@example.com', age: 22 }
];

// リゾルバー(実際にデータを取得する処理)
const resolvers = {
  Query: {
    // すべてのユーザーを返す関数
    users: () => {
      console.log('全ユーザーが要求されました');
      return sampleUsers;
    },
    
    // 特定のユーザーを返す関数
    user: (parent, args) => {
      console.log(`ID: ${args.id} のユーザーが要求されました`);
      // 配列から指定されたIDのユーザーを探す
      return sampleUsers.find(user => user.id === args.id);
    }
  }
};

// サーバーを起動する関数
async function startServer() {
  // Apollo Serverを作成
  const server = new ApolloServer({ 
    typeDefs,    // スキーマ定義
    resolvers    // データ取得処理
  });

  // Expressアプリを作成
  const app = express();
  
  // Apollo Serverを開始
  await server.start();
  
  // ExpressアプリにApollo Serverを統合
  server.applyMiddleware({ app });

  const PORT = 4000;
  
  // サーバーを起動
  app.listen(PORT, () => {
    console.log(`🚀 GraphQLサーバーが起動しました!`);
    console.log(`📍 GraphQL Playground: http://localhost:${PORT}${server.graphqlPath}`);
  });
}

// サーバーを起動
startServer().catch(error => {
  console.error('サーバー起動エラー:', error);
});

うわー、結構長いコードですね!

そうだね!実は構造はシンプルなんだ。大きく分けて3つの部分があるよ。

  1. typeDefs:「こんなデータがありますよ」という設計図
  2. resolvers:「実際にデータを取得する処理」
  3. サーバー起動処理:「サーバーを立ち上げる」

1.必要なライブラリの読み込み

// server.js
const { ApolloServer, gql } = require('apollo-server-express');
const express = require('express');

この最初の2行は何をしているんですか?

これは「分割代入」という書き方だね。

// これは以下と同じ意味
// const apolloServerExpress = require('apollo-server-express');
// const ApolloServer = apolloServerExpress.ApolloServer;
// const gql = apolloServerExpress.gql;

const { ApolloServer, gql } = require('apollo-server-express');

ApolloServer:GraphQLサーバーを作るためのクラス
gql:GraphQLスキーマを書くためのテンプレートリテラル関数
●express:WebサーバーのためのNode.jsフレームワーク

2.GraphQLスキーマの定義

// GraphQLのスキーマを定義(どんなデータがあるかを決める設計図)
const typeDefs = gql`
  # ユーザーという型を定義
  type User {
    id: ID!          # IDは必須項目(!マークで必須を表す)
    name: String!    # 名前は文字列で必須
    email: String!   # メールアドレスも文字列で必須
    age: Int         # 年齢は整数(必須ではない)
  }

  # クエリ(データを取得する処理)を定義
  type Query {
    # すべてのユーザーを取得する
    users: [User!]!  # User型の配列を返す、必須
    
    # 特定のIDのユーザーを取得する
    user(id: ID!): User  # IDを受け取って、Userを返す(見つからない場合はnull)
  }
`;

このgqlの部分、変わった書き方ですね!

これは「テンプレートリテラル」という書き方だよ。
バッククォート(`)で囲むことで、複数行の文字列を書けるんだ。

// 普通の文字列だとこうなる(読みにくい!)
const typeDefs = "type User { id: ID! name: String! }";

// テンプレートリテラルなら読みやすい
const typeDefs = gql`
  type User {
    id: ID!
    name: String!
  }
`;

[User!]!の部分が分からないです…

GraphQLの型表記だね!詳しく見てみよう

// 型の表記方法
String     // 文字列(nullの可能性あり)
String!    // 文字列(必須、nullは不可)
[String]   // 文字列の配列(配列自体がnullの可能性あり)
[String!]  // 文字列の配列(配列内の要素はnull不可、配列自体はnullの可能性あり)
[String!]! // 文字列の配列(配列内の要素もnull不可、配列自体もnull不可)

// 具体例
users: [User!]!
// ↑ これは「User型の配列で、配列内にnullは入らず、配列自体もnullではない」という意味

えぇ!?待ってください!
User型の中でage: Intって必須じゃないのに、
[User!]!って書いて大丈夫なんですか?

これは多くの人が混乱するポイントなんだ。
実は全然問題ないよ。理由を説明するね。

// User型の定義
type User {
  id: ID!      // 必須
  name: String! // 必須
  email: String! // 必須
  age: Int     // オプショナル(nullの可能性あり)
}

// この場合、以下のデータは全て有効なUser型
const validUsers = [
  { id: "1", name: "山田", email: "yamada@example.com", age: 25 },     // ageあり
  { id: "2", name: "田中", email: "tanaka@example.com", age: null },   // ageがnull
  { id: "3", name: "佐藤", email: "sato@example.com" }                // ageがundefined
];

// でも、以下は無効(User型ではない)
const invalidData = [
  null,        // これはUser型ではない
  undefined,   // これもUser型ではない
  "文字列"      // これもUser型ではない
];

あー!つまりUser!は「User型のオブジェクトであることは保証する」けど、「User型の中身のフィールドがnullかどうかは別問題」ってことですね!

完璧な理解だ!もう少し具体的に見てみよう!

// [User!]! の意味を分解すると...

// 1. User! → 「User型のオブジェクトで、nullではない」
//    ✅ { id: "1", name: "山田", email: "yamada@example.com", age: 25 }
//    ✅ { id: "2", name: "田中", email: "tanaka@example.com", age: null }
//    ❌ null
//    ❌ undefined

// 2. [User!] → 「User型のオブジェクトの配列で、配列内にnullは含まない」
//    ✅ [user1, user2, user3]
//    ❌ [user1, null, user3]  // 配列内にnullがある
//    ✅ []  // 空配列はOK

// 3. [User!]! → 「上記の配列で、配列自体もnullではない」
//    ✅ [user1, user2, user3]
//    ✅ []  // 空配列
//    ❌ null  // 配列自体がnull

なるほど!「型の保証」と「フィールドの値の保証」は別次元の話なんですね!

そういうこと!GraphQLでは、こんな風に段階的に型安全性を設計できるんだ!

// 様々なパターンの例
type Query {
  // パターン1: 配列もUser型も必須
  allUsers: [User!]!        // 必ず配列が返る、中身は必ずUser型
  
  // パターン2: 配列は必須だが、User型はnullの可能性あり
  someUsers: [User]!        // 必ず配列が返る、中身はUser型かnull
  
  // パターン3: 配列自体がnullの可能性あり
  maybeUsers: [User!]       // 配列かnull、配列の場合は中身は必ずUser型
  
  // パターン4: 全てがnullの可能性あり
  optionalUsers: [User]     // 配列かnull、配列の場合は中身もnullの可能性あり
}

// 実際のレスポンス例
{
  "allUsers": [
    { "id": "1", "name": "山田", "email": "yamada@example.com", "age": 25 },
    { "id": "2", "name": "田中", "email": "tanaka@example.com", "age": null }
  ],
  "someUsers": [
    { "id": "1", "name": "山田", "email": "yamada@example.com", "age": 25 },
    null,  // これはOK
    { "id": "3", "name": "佐藤", "email": "sato@example.com", "age": null }
  ],
  "maybeUsers": null,  // 配列自体がnullでもOK
  "optionalUsers": [
    { "id": "1", "name": "山田", "email": "yamada@example.com", "age": 25 },
    null   // これもOK
  ]
}

3.サンプルデータの準備

// サンプルデータ(本来はデータベースから取得)
const sampleUsers = [
  { id: '1', name: '山田太郎', email: 'yamada@example.com', age: 25 },
  { id: '2', name: '田中花子', email: 'tanaka@example.com', age: 30 },
  { id: '3', name: '佐藤次郎', email: 'sato@example.com', age: 22 }
];

今回は簡単にするために、メモリ上に配列でデータを用意したよ。本格的なアプリではデータベースを使うけどね。

4.リゾルバーの実装

// リゾルバー(実際にデータを取得する処理)
const resolvers = {
  Query: {
    // すべてのユーザーを返す関数
    users: () => {
      console.log('全ユーザーが要求されました');
      return sampleUsers;
    },
    
    // 特定のユーザーを返す関数
    user: (parent, args) => {
      console.log(`ID: ${args.id} のユーザーが要求されました`);
      // 配列から指定されたIDのユーザーを探す
      return sampleUsers.find(user => user.id === args.id);
    }
  }
};

(parent, args)って何ですか?

GraphQLのリゾルバー関数は、4つの引数を受け取るんだ。

// リゾルバー関数の引数
function myResolver(parent, args, context, info) {
  // parent: 親のリゾルバーからの結果(今回は使わない)
  // args: クエリで渡された引数(user(id: "1") の "1" など)
  // context: 全リゾルバーで共有するデータ(認証情報など)
  // info: クエリの詳細情報(高度な使い方で使用)
}

// 実際の使用例
user: (parent, args) => {
  console.log('受け取った引数:', args); // { id: "1" }
  return sampleUsers.find(user => user.id === args.id);
}

でも待ってください!スキーマではuser(id: ID!): Userって書いてるけど、なんでリゾルバーでは(parent, args)って書くんですか?user(id)じゃダメなんですか?

これはGraphQLの仕組みを理解する上で重要なポイントなんだ。詳しく説明するね。

スキーマ vs リゾルバーの関係

// 【スキーマ】= 「外部への約束・インターフェース」
type Query {
  user(id: ID!): User  // 「IDを受け取ってUserを返しますよ」という約束
}

// 【リゾルバー】= 「約束を実現する実際の処理」
const resolvers = {
  Query: {
    user: (parent, args) => {
      // args.id でスキーマで定義した引数にアクセス
      return sampleUsers.find(user => user.id === args.id);
    }
  }
};

あー!スキーマは「約束」で、リゾルバーは「実装」って感じのイメージですね!

その通り!もう少し具体的に見てみよう!

// スキーマで色々な引数パターンを定義できる
type Query {
  # 引数なし
  users: [User!]!
  
  # 引数1つ
  user(id: ID!): User
  
  # 引数2つ
  userByEmail(email: String!, domain: String!): User
  
  # オプショナル引数
  searchUsers(name: String, minAge: Int): [User!]!
}

// リゾルバーでは全部同じパターン
const resolvers = {
  Query: {
    // 引数なし → args は空オブジェクト {}
    users: (parent, args) => {
      console.log(args); // {}
      return sampleUsers;
    },
    
    // 引数1つ → args は { id: "値" }
    user: (parent, args) => {
      console.log(args); // { id: "1" }
      return sampleUsers.find(user => user.id === args.id);
    },
    
    // 引数2つ → args は { email: "値", domain: "値" }
    userByEmail: (parent, args) => {
      console.log(args); // { email: "test@example.com", domain: "example.com" }
      return sampleUsers.find(user => 
        user.email === args.email && user.email.endsWith(args.domain)
      );
    },
    
    // オプショナル引数 → 指定されなかった引数はundefined
    searchUsers: (parent, args) => {
      console.log(args); // { name: "山田", minAge: undefined } など
      let result = sampleUsers;
      
      if (args.name) {
        result = result.filter(user => user.name.includes(args.name));
      }
      
      if (args.minAge !== undefined) {
        result = result.filter(user => user.age >= args.minAge);
      }
      
      return result;
    }
  }
};

なるほど!引数の数に関係なく、リゾルバーは常に(parent, args)で受け取るんですね!

どうしてこういった仕組みなの?

GraphQLがこの仕組みを採用している理由は、統一性と柔軟性だよ。

// もしJavaScriptの関数のように直接引数を受け取る場合...
const badExample = {
  Query: {
    user: (id) => { /* ... */ },           // 引数1つ
    userByEmail: (email, domain) => { /* ... */ }, // 引数2つ
    searchUsers: (name, minAge) => { /* ... */ }   // オプショナル引数は?
  }
};

// 問題点:
// 1. 引数の数がバラバラで統一性がない
// 2. オプショナル引数の扱いが難しい
// 3. 将来的に引数を追加する時に既存コードが壊れる
// GraphQLの方式なら...
const goodExample = {
  Query: {
    // 全て同じパターン!統一性がある
    user: (parent, args) => { /* args.id */ },
    userByEmail: (parent, args) => { /* args.email, args.domain */ },
    searchUsers: (parent, args) => { /* args.name, args.minAge */ }
  }
};

// メリット:
// 1. 全てのリゾルバーが同じ形
// 2. 引数の追加/削除が簡単
// 3. ツールでの自動生成がしやすい

統一性があることで、コードが書きやすくなるんですね!
でもparentって何に使うんですか?

parentは、ネストしたクエリで威力を発揮するんだ。例えば、、

// こんなクエリが来た場合
query {
  user(id: "1") {
    name
    posts {      # ←ここでparentが活躍!
      title
    }
  }
}

// スキーマ定義
type User {
  id: ID!
  name: String!
  posts: [Post!]!  # ←このpostsリゾルバーでparentを使う
}

// リゾルバー
const resolvers = {
  Query: {
    user: (parent, args) => {
      return sampleUsers.find(user => user.id === args.id);
    }
  },
  User: {
    // User型のpostsフィールドのリゾルバー
    posts: (parent, args) => {
      // parent は上のuserリゾルバーが返したUserオブジェクト
      console.log(parent); // { id: "1", name: "山田太郎", email: "..." }
      
      // そのユーザーの投稿を取得
      return samplePosts.filter(post => post.authorId === parent.id);
    }
  }
};

あー!parentがあることで、関連するデータを芋づる式に取得できるんですね!

5.サーバー起動処理

// サーバーを起動する関数
async function startServer() {
  // Apollo Serverを作成
  const server = new ApolloServer({ 
    typeDefs,    // スキーマ定義
    resolvers    // データ取得処理
  });

  // Expressアプリを作成
  const app = express();
  
  // Apollo Serverを開始
  await server.start();
  
  // ExpressアプリにApollo Serverを統合
  server.applyMiddleware({ app });

  const PORT = 4000;
  
  // サーバーを起動
  app.listen(PORT, () => {
    console.log(`🚀 GraphQLサーバーが起動しました!`);
    console.log(`📍 GraphQL Playground: http://localhost:${PORT}${server.graphqlPath}`);
  });
}

server.applyMiddleware({ app })は何をしているんですか?

Apollo ServerをExpressアプリに組み込む処理だよ。

// applyMiddleware を実行すると...
server.applyMiddleware({ app });

// 以下のようなルートが自動的に作られる
// GET /graphql  → GraphQL Playground画面
// POST /graphql → GraphQLクエリの実行

6.エラーハンドリング

// サーバーを起動
startServer().catch(error => {
  console.error('サーバー起動エラー:', error);
});

async関数はPromiseを返すから、エラーが起きた時のために.catch()でキャッチしてるんだ。

うわー、一つ一つ説明してもらうと、すごく分かりやすいです!全体的にはシンプルな構造なんですね。

そうだね!実は構造はシンプルなんだ。もう一度言うけど、大きく分けて3つの部分があるのでしっかりと頭に入れておこう!!

  1. typeDefs:「こんなデータがありますよ」という設計図
  2. resolvers:「実際にデータを取得する処理」
  3. サーバー起動処理:「サーバーを立ち上げる」

package.jsonを更新

開発を楽にするために、package.jsonにスクリプトを追加しよう。

{
  "name": "my-first-graphql",
  "version": "1.0.0",
  "description": "",
  "main": "server.js",
  "scripts": {
    "start": "node server.js",
    "dev": "nodemon server.js"
  },
  "dependencies": {
    "apollo-server-express": "^latest",
    "express": "^latest",
    "graphql": "^latest"
  },
  "devDependencies": {
    "nodemon": "^latest"
  }
}

サーバーを起動してみよう!

それでは、サーバーを起動してみよう!

npm run dev

おお!ターミナルに「GraphQLサーバーが起動しました!」って表示されました!

OK!じゃあ、ブラウザで http://localhost:4000/graphql を開いてみて。

GraphQL Playgroundで実際にクエリを実行

すごい!なんか格好いい画面が開きました!

これが「GraphQL Playground」だよ。
ここで実際にクエリを書いて、データを取得してみよう。

クエリその1:全ユーザーを取得

# 左側のエディタに以下を入力
query {
  users {
    id
    name
    email
  }
}

このクエリを入力して、真ん中の再生ボタン(▶)をクリックしてみて。

うわー!右側にデータが表示されました!

{
  "data": {
    "users": [
      {
        "id": "1",
        "name": "山田太郎",
        "email": "yamada@example.com"
      },
      {
        "id": "2",
        "name": "田中花子",
        "email": "tanaka@example.com"
      },
      {
        "id": "3",
        "name": "佐藤次郎",
        "email": "sato@example.com"
      }
    ]
  }
}

クエリその2:特定のユーザーを取得

今度は特定のユーザーだけを取得してみよう。

query {
  user(id: "1") {
    name
    email
    age
  }
}

今度は山田太郎さんの情報だけが返ってきました!

{
  "data": {
    "user": {
      "name": "山田太郎",
      "email": "yamada@example.com",
      "age": 25
    }
  }
}

クエリその3:欲しいフィールドだけ取得

じゃあ、GraphQLの醍醐味を味わってみよう。名前だけが欲しい場合は?

query {
  users {
    name
    # emailやageは取得しない
  }
}

本当に名前だけが返ってきました!
これがオーバーフェッチングを防ぐってことですね!

{
  "data": {
    "users": [
      { "name": "山田太郎" },
      { "name": "田中花子" },
      { "name": "佐藤次郎" }
    ]
  }
}

エラーハンドリングも確認

じゃあ、存在しないIDを指定するとどうなるか試してみよう。

query {
  user(id: "999") {
    name
    email
  }
}

結果がnullになりました!エラーにならずに、きちんと「見つからない」ことが分かりますね。

{
  "data": {
    "user": null
  }
}

スキーマの自動補完機能

GraphQL Playgroundの右側にある「DOCS」タブをクリックしてみて。

おお!自動的にAPIの仕様書が作られてる!どんなクエリが使えるか、どんなデータが返ってくるかが全部書いてあります!

そうなんだ!これもGraphQLの大きな利点の一つ。APIの仕様が自動的に生成されるから、フロントエンドの開発者も迷わずに済むんだ。

今日のまとめ

今日学んだことをまとめてみよう!

作ったもの

  • Node.js + Apollo Serverを使ったGraphQLサーバー
  • User型のスキーマ定義
  • 全ユーザー取得と特定ユーザー取得のクエリ

学んだ概念

  • typeDefs:データの型を定義する設計図
  • resolvers:実際にデータを取得する処理
  • GraphQL Playground:クエリをテストできるツール

GraphQLの利点を実感

  • 必要なフィールドだけ取得できる
  • 自動的にAPIドキュメントが生成される
  • 型安全でエラーが分かりやすい

実際に動くものが作れて、すごく楽しかったです!
でも、実際のアプリではデータベースを使いますよね

その通り!
次回はデータベースと連携して、もっと実践的なGraphQLサーバーを作ってみよう