LESSON 40分

ストーリー

高橋アーキテクト
RESTは素晴らしい設計パターンだ。でも万能じゃない

高橋アーキテクトが複雑な画面のモックアップを見せた。 ダッシュボード画面には、ユーザー情報、最近のタスク、プロジェクト統計、通知が表示されている。

高橋アーキテクト
この画面を表示するのに、REST APIだと何回リクエストが必要だ?
あなた
えっと…ユーザー情報、タスク一覧、プロジェクト統計、通知…4回ですか?
高橋アーキテクト
そうだ。場合によってはもっと増える。GraphQLなら、1回のリクエストで必要なデータだけを取得できる。2015年にFacebookが公開した、クエリ言語だ

GraphQLとは

RESTの課題

// ダッシュボード画面に必要なデータを取得する場合

// REST: 複数のエンドポイントにリクエスト
const user = await fetch('/api/v1/users/me');
const tasks = await fetch('/api/v1/users/me/tasks?limit=5');
const projects = await fetch('/api/v1/projects?limit=3');
const notifications = await fetch('/api/v1/notifications?unread=true');

// 問題1: Over-fetching(不要なデータも取得する)
// ユーザー情報のうち、名前とアバターだけ欲しいのに全フィールドが返る

// 問題2: Under-fetching(データが足りず追加リクエストが必要)
// タスクの担当者名を表示するために、各タスクのユーザー情報も取得が必要

GraphQLのアプローチ

# GraphQL: 1回のリクエストで必要なデータだけ取得
query DashboardData {
  me {
    name
    avatarUrl
  }
  myTasks(limit: 5) {
    id
    title
    status
    assignee {
      name
    }
  }
  projects(limit: 3) {
    id
    name
    taskCount
  }
  notifications(unread: true) {
    id
    message
    createdAt
  }
}

GraphQLの3つの操作

1. Query(データ取得)

# Query: データの読み取り(RESTのGETに相当)

# ユーザー情報を取得
query GetUser {
  user(id: "usr_123") {
    id
    name
    email
    tasks {
      id
      title
      status
    }
  }
}

# レスポンス
{
  "data": {
    "user": {
      "id": "usr_123",
      "name": "田中太郎",
      "email": "tanaka@example.com",
      "tasks": [
        { "id": "tsk_1", "title": "API設計", "status": "IN_PROGRESS" },
        { "id": "tsk_2", "title": "テスト作成", "status": "TODO" }
      ]
    }
  }
}

2. Mutation(データ変更)

# Mutation: データの作成・更新・削除(RESTのPOST/PUT/DELETEに相当)

# タスクを作成
mutation CreateTask {
  createTask(input: {
    projectId: "proj_123"
    title: "ログイン画面の改善"
    priority: HIGH
    assigneeId: "usr_456"
  }) {
    id
    title
    status
    priority
    assignee {
      name
    }
  }
}

# レスポンス
{
  "data": {
    "createTask": {
      "id": "tsk_789",
      "title": "ログイン画面の改善",
      "status": "TODO",
      "priority": "HIGH",
      "assignee": {
        "name": "鈴木花子"
      }
    }
  }
}

3. Subscription(リアルタイム通知)

# Subscription: リアルタイムのデータ更新通知(WebSocket)

# タスクのステータス変更を購読
subscription OnTaskUpdated {
  taskUpdated(projectId: "proj_123") {
    id
    title
    status
    updatedBy {
      name
    }
  }
}

# タスクが更新されるたびにデータが配信される
{
  "data": {
    "taskUpdated": {
      "id": "tsk_789",
      "title": "ログイン画面の改善",
      "status": "IN_PROGRESS",
      "updatedBy": { "name": "鈴木花子" }
    }
  }
}

TypeScriptでの実装

サーバー側(Apollo Server)

import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';

// 型定義(スキーマ)
const typeDefs = `#graphql
  type User {
    id: ID!
    name: String!
    email: String!
    tasks: [Task!]!
  }

  type Task {
    id: ID!
    title: String!
    description: String
    status: TaskStatus!
    priority: Priority!
    assignee: User
    createdAt: String!
  }

  enum TaskStatus {
    TODO
    IN_PROGRESS
    IN_REVIEW
    DONE
  }

  enum Priority {
    LOW
    MEDIUM
    HIGH
    CRITICAL
  }

  type Query {
    user(id: ID!): User
    tasks(projectId: ID!, limit: Int): [Task!]!
  }

  input CreateTaskInput {
    projectId: ID!
    title: String!
    description: String
    priority: Priority
    assigneeId: ID
  }

  type Mutation {
    createTask(input: CreateTaskInput!): Task!
    updateTaskStatus(id: ID!, status: TaskStatus!): Task!
  }
`;

// リゾルバー(各フィールドのデータ取得ロジック)
const resolvers = {
  Query: {
    user: async (_: unknown, { id }: { id: string }) => {
      return await userRepository.findById(id);
    },
    tasks: async (_: unknown, { projectId, limit }: { projectId: string; limit?: number }) => {
      return await taskRepository.findByProject(projectId, limit);
    },
  },
  Mutation: {
    createTask: async (_: unknown, { input }: { input: CreateTaskInput }) => {
      return await taskService.create(input);
    },
  },
  User: {
    tasks: async (parent: User) => {
      return await taskRepository.findByUserId(parent.id);
    },
  },
};

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

クライアント側

// GraphQLクエリの実行
async function fetchDashboard(): Promise<DashboardData> {
  const response = await fetch('http://localhost:4000/graphql', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${token}`,
    },
    body: JSON.stringify({
      query: `
        query Dashboard {
          user(id: "usr_123") {
            name
            email
          }
          tasks(projectId: "proj_123", limit: 5) {
            id
            title
            status
          }
        }
      `,
    }),
  });

  const result = await response.json();
  return result.data;
}

GraphQLの型システム

スカラー型

# 組み込みスカラー型
type Example {
  id: ID!              # 一意の識別子
  name: String!         # 文字列(! は非null必須)
  age: Int              # 整数(! がないのでnull許容)
  price: Float          # 浮動小数点
  isActive: Boolean!    # 真偽値
}

# カスタムスカラー型
scalar DateTime         # 日付・時刻
scalar JSON             # 任意のJSON

修飾子

type Task {
  title: String!        # 非null(必ず値がある)
  description: String   # null許容(値がない場合がある)
  tags: [String!]!      # 非nullの文字列配列(配列自体も非null)
  labels: [Label]       # null許容のLabel配列(配列自体もnull許容)
}

まとめ

ポイント内容
GraphQLクライアントが必要なデータだけを指定して取得できるクエリ言語
Queryデータの読み取り(RESTのGET相当)
Mutationデータの変更(RESTのPOST/PUT/DELETE相当)
Subscriptionリアルタイム通知(WebSocket)
型システムスカラー型、オブジェクト型、列挙型、Input型
メリットOver-fetching/Under-fetchingの解決、1リクエストで複数データ取得

チェックリスト

  • GraphQLの3つの操作(Query, Mutation, Subscription)を理解した
  • RESTの課題(Over-fetching, Under-fetching)を説明できる
  • GraphQLの型システム(スカラー型、修飾子)を理解した
  • サーバー側(Apollo Server)の基本構造を把握した

次のステップへ

GraphQLの基本概念を学びました。

次のセクションでは、スキーマ設計とリゾルバーの詳細を学びます。 効率的なスキーマ設計が、GraphQL APIの品質を決定します。


推定読了時間: 40分