GraphQL入門|REST APIとの違いと実装方法をわかりやすく解説

kento_morota 23分で読めます

フロントエンドから必要なデータだけを効率よく取得したい、複数のAPIリクエストを1回にまとめたい——こうした課題を解決するために生まれたのがGraphQLです。Meta(旧Facebook)が2015年に公開したこの技術は、現在多くの企業で採用されています。

本記事では、GraphQLの基本概念からREST APIとの比較、スキーマ設計、サーバー・クライアント双方の実装方法までを体系的に解説します。

GraphQLとは何か

GraphQLは、APIのためのクエリ言語であり、サーバーサイドのランタイムです。クライアントが必要なデータの構造を指定し、サーバーがその構造に合わせたレスポンスを返すという仕組みが特徴です。

GraphQLが解決する課題

REST APIには以下のような課題があります。GraphQLはこれらを根本的に解決します。

オーバーフェッチ(取りすぎ)
REST APIでは、エンドポイントごとに返すデータが固定されています。ユーザー名だけが必要な場合でも、/api/users/1 にリクエストすると、メールアドレスや住所など不要なデータも含めて全フィールドが返されます。

アンダーフェッチ(足りない)
1つの画面に表示する情報を揃えるために、複数のエンドポイントに別々にリクエストしなければなりません。ユーザー情報、投稿一覧、コメント一覧をそれぞれ別のAPIで取得するといった具合です。

フロントエンドの多様化
Webアプリ、モバイルアプリ、ウェアラブルデバイスなど、各クライアントで必要なデータが異なります。REST APIでは、クライアントごとにエンドポイントを用意するか、汎用的すぎるレスポンスを返すかの二択を迫られます。

GraphQLの基本的な仕組み

GraphQLでは、クライアントがクエリで必要なデータの構造を宣言し、サーバーがそのとおりのデータを返します。

# GraphQLクエリの例
query {
  user(id: "1") {
    name
    email
    posts {
      title
      createdAt
    }
  }
}
// レスポンス(クエリと同じ構造で返る)
{
  "data": {
    "user": {
      "name": "田中太郎",
      "email": "tanaka@example.com",
      "posts": [
        {
          "title": "GraphQLを使ってみた",
          "createdAt": "2026-03-20"
        },
        {
          "title": "REST APIからの移行記録",
          "createdAt": "2026-03-15"
        }
      ]
    }
  }
}

このように、1回のリクエストで必要なデータを過不足なく取得できます。REST APIでは/api/users/1/api/users/1/posts の2回のリクエストが必要だったものが、GraphQLでは1回で済みます。

REST APIとGraphQLの詳細比較

REST APIとGraphQLの違いを複数の観点から比較します。

アーキテクチャの違い

エンドポイント
REST APIはリソースごとに複数のエンドポイント(/users, /posts, /comments)を持ちます。GraphQLは単一のエンドポイント(通常 /graphql)で、すべてのデータ操作を処理します。

HTTPメソッド
REST APIはGET/POST/PUT/DELETEなどのHTTPメソッドで操作を区別します。GraphQLはQuery(読み取り)、Mutation(変更)、Subscription(リアルタイム通知)の3種類の操作があり、通常すべてPOSTメソッドで送信します。

データ形式
REST APIではサーバーがレスポンスの形を決定します。GraphQLではクライアントがクエリで形を指定します。

それぞれの強みと弱み

REST APIの強み

  • シンプルで直感的。HTTPの仕組みを活用しているため学習コストが低い
  • HTTPキャッシュ(ブラウザキャッシュ、CDNキャッシュ)を自然に活用できる
  • ファイルアップロードが容易
  • ほぼすべてのプログラミング言語・フレームワークで簡単に実装できる

GraphQLの強み

  • 必要なデータだけを過不足なく取得できる
  • 1回のリクエストで関連データをまとめて取得できる
  • 型システムによる厳密なスキーマ定義がある
  • フロントエンドの自由度が高く、バックエンドの変更なしにデータ取得を調整できる
  • API仕様が自動的にドキュメント化される(イントロスペクション)

GraphQLの弱み

  • HTTPキャッシュが効きにくい(POSTリクエストのため)
  • 学習コストがREST APIより高い
  • N+1問題への対策が必要
  • ファイルアップロードの標準的な方法がない
  • シンプルなCRUD操作には過剰な場合がある

GraphQLスキーマの設計

GraphQLの中心にあるのがスキーマです。スキーマはAPIの型定義であり、どのようなデータを取得・操作できるかを宣言します。

型システムの基本

# スカラー型(基本型)
# String, Int, Float, Boolean, ID が組み込み型として用意されている

# オブジェクト型の定義
type User {
  id: ID!                # !は非null(必須)を意味する
  name: String!
  email: String!
  age: Int
  posts: [Post!]!        # Post型の配列(空配列は許可、null要素は不可)
  createdAt: DateTime!   # カスタムスカラー型
}

type Post {
  id: ID!
  title: String!
  content: String!
  author: User!
  comments: [Comment!]!
  tags: [String!]
  publishedAt: DateTime
}

type Comment {
  id: ID!
  body: String!
  author: User!
  post: Post!
  createdAt: DateTime!
}

# カスタムスカラー型の定義
scalar DateTime

Query・Mutation・Subscriptionの定義

# Query:データの読み取り
type Query {
  # 単一ユーザーの取得
  user(id: ID!): User

  # ユーザー一覧の取得(ページネーション付き)
  users(limit: Int = 10, offset: Int = 0): UserConnection!

  # 投稿の検索
  searchPosts(keyword: String!, limit: Int = 10): [Post!]!
}

# Mutation:データの変更
type Mutation {
  # ユーザーの作成
  createUser(input: CreateUserInput!): User!

  # 投稿の作成
  createPost(input: CreatePostInput!): Post!

  # 投稿の更新
  updatePost(id: ID!, input: UpdatePostInput!): Post!

  # 投稿の削除
  deletePost(id: ID!): Boolean!
}

# Input型(Mutationの引数用)
input CreateUserInput {
  name: String!
  email: String!
  age: Int
}

input CreatePostInput {
  title: String!
  content: String!
  tags: [String!]
}

input UpdatePostInput {
  title: String
  content: String
  tags: [String!]
}

# Subscription:リアルタイム通知
type Subscription {
  postCreated: Post!
  commentAdded(postId: ID!): Comment!
}

# ページネーション用の型(Relay仕様)
type UserConnection {
  edges: [UserEdge!]!
  pageInfo: PageInfo!
  totalCount: Int!
}

type UserEdge {
  node: User!
  cursor: String!
}

type PageInfo {
  hasNextPage: Boolean!
  hasPreviousPage: Boolean!
  startCursor: String
  endCursor: String
}

サーバーサイドの実装(Node.js + Apollo Server)

Node.jsとApollo Serverを使ったGraphQLサーバーの実装方法を紹介します。

プロジェクトのセットアップ

# プロジェクトの初期化
mkdir graphql-server && cd graphql-server
npm init -y
npm install @apollo/server graphql
npm install -D typescript @types/node ts-node

リゾルバの実装

リゾルバは、スキーマの各フィールドにデータを返す関数です。

// src/index.ts
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';

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

  type Post {
    id: ID!
    title: String!
    content: String!
    author: User!
  }

  type Query {
    users: [User!]!
    user(id: ID!): User
    posts: [Post!]!
    post(id: ID!): Post
  }

  type Mutation {
    createPost(title: String!, content: String!, authorId: ID!): Post!
  }
`;

// サンプルデータ
const users = [
  { id: '1', name: '田中太郎', email: 'tanaka@example.com' },
  { id: '2', name: '佐藤花子', email: 'sato@example.com' },
];

const posts = [
  { id: '1', title: 'GraphQL入門', content: 'GraphQLは...', authorId: '1' },
  { id: '2', title: 'REST vs GraphQL', content: 'RESTとGraphQLの...', authorId: '2' },
];

// リゾルバ
const resolvers = {
  Query: {
    users: () => users,
    user: (_: unknown, { id }: { id: string }) =>
      users.find(user => user.id === id),
    posts: () => posts,
    post: (_: unknown, { id }: { id: string }) =>
      posts.find(post => post.id === id),
  },

  Mutation: {
    createPost: (_: unknown, args: { title: string; content: string; authorId: string }) => {
      const newPost = {
        id: String(posts.length + 1),
        title: args.title,
        content: args.content,
        authorId: args.authorId,
      };
      posts.push(newPost);
      return newPost;
    },
  },

  // フィールドレベルのリゾルバ
  User: {
    posts: (parent: { id: string }) =>
      posts.filter(post => post.authorId === parent.id),
  },

  Post: {
    author: (parent: { authorId: string }) =>
      users.find(user => user.id === parent.authorId),
  },
};

// サーバーの起動
const server = new ApolloServer({ typeDefs, resolvers });

const startServer = async () => {
  const { url } = await startStandaloneServer(server, {
    listen: { port: 4000 },
  });
  console.log(`GraphQLサーバー起動: ${url}`);
};

startServer();

N+1問題とDataLoaderによる解決

GraphQLで最もよく問題になるのがN+1問題です。投稿一覧を取得する際に、各投稿の著者情報を個別にデータベースから取得してしまうと、投稿数+1回のクエリが発生します。

// DataLoaderを使ったN+1問題の解決
import DataLoader from 'dataloader';

// バッチ関数:複数のIDをまとめて1回のクエリで取得
const createUserLoader = () => new DataLoader(async (userIds: readonly string[]) => {
  // 1回のSQLクエリで全ユーザーを取得
  const users = await db.query(
    'SELECT * FROM users WHERE id IN (?)',
    [userIds]
  );

  // IDの順序に合わせてマッピング
  const userMap = new Map(users.map(u => [u.id, u]));
  return userIds.map(id => userMap.get(id) || null);
});

// リクエストごとにDataLoaderのインスタンスを生成
const server = new ApolloServer({ typeDefs, resolvers });

const { url } = await startStandaloneServer(server, {
  context: async () => ({
    loaders: {
      userLoader: createUserLoader(),
    },
  }),
});

// リゾルバでDataLoaderを使用
const resolvers = {
  Post: {
    author: (parent, _, context) =>
      context.loaders.userLoader.load(parent.authorId),
  },
};

DataLoaderは同一リクエスト内のload呼び出しを自動的にバッチ化し、1回のSQLクエリにまとめます。これにより、N+1のクエリが2回(投稿一覧の取得 + ユーザー一括取得)に削減されます。

クライアントサイドの実装(React + Apollo Client)

フロントエンドからGraphQLを利用する方法を、ReactとApollo Clientを使って解説します。

セットアップと基本的なクエリ

// src/apolloClient.ts
import { ApolloClient, InMemoryCache, gql } from '@apollo/client';

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

export default client;
// src/App.tsx
import { ApolloProvider } from '@apollo/client';
import client from './apolloClient';
import UserList from './components/UserList';

function App() {
  return (
    <ApolloProvider client={client}>
      <UserList />
    </ApolloProvider>
  );
}
// src/components/UserList.tsx
import { useQuery, gql } from '@apollo/client';

const GET_USERS = gql`
  query GetUsers {
    users {
      id
      name
      email
      posts {
        id
        title
      }
    }
  }
`;

function UserList() {
  const { loading, error, data } = useQuery(GET_USERS);

  if (loading) return <p>読み込み中...</p>;
  if (error) return <p>エラー: {error.message}</p>;

  return (
    <ul>
      {data.users.map((user) => (
        <li key={user.id}>
          <h3>{user.name}</h3>
          <p>{user.email}</p>
          <p>投稿数: {user.posts.length}</p>
        </li>
      ))}
    </ul>
  );
}

Mutationの実装

// src/components/CreatePost.tsx
import { useMutation, gql } from '@apollo/client';
import { useState } from 'react';

const CREATE_POST = gql`
  mutation CreatePost($title: String!, $content: String!, $authorId: ID!) {
    createPost(title: $title, content: $content, authorId: $authorId) {
      id
      title
      content
      author {
        name
      }
    }
  }
`;

function CreatePost() {
  const [title, setTitle] = useState('');
  const [content, setContent] = useState('');

  const [createPost, { loading, error }] = useMutation(CREATE_POST, {
    // キャッシュの自動更新
    update(cache, { data: { createPost } }) {
      cache.modify({
        fields: {
          posts(existingPosts = []) {
            const newPostRef = cache.writeFragment({
              data: createPost,
              fragment: gql`
                fragment NewPost on Post {
                  id
                  title
                  content
                }
              `,
            });
            return [...existingPosts, newPostRef];
          },
        },
      });
    },
  });

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    await createPost({
      variables: { title, content, authorId: '1' },
    });
    setTitle('');
    setContent('');
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={title}
        onChange={(e) => setTitle(e.target.value)}
        placeholder="タイトル"
      />
      <textarea
        value={content}
        onChange={(e) => setContent(e.target.value)}
        placeholder="本文"
      />
      <button type="submit" disabled={loading}>
        {loading ? '投稿中...' : '投稿する'}
      </button>
      {error && <p>エラー: {error.message}</p>}
    </form>
  );
}

GraphQLのセキュリティ対策

GraphQLを本番運用する際に必ず検討すべきセキュリティ対策を紹介します。

クエリの深さ制限と複雑度制限

GraphQLはクライアントが自由にクエリを組み立てられるため、悪意のあるユーザーが深くネストしたクエリを送信してサーバーを過負荷にする攻撃が可能です。

# 悪意のあるクエリの例(深いネスト)
query {
  user(id: "1") {
    posts {
      author {
        posts {
          author {
            posts {
              # 無限に深くネスト可能
            }
          }
        }
      }
    }
  }
}

対策として、クエリの深さ制限と複雑度制限を実装します。

// クエリの深さ制限を導入する例
import depthLimit from 'graphql-depth-limit';
import { createComplexityLimitRule } from 'graphql-validation-complexity';

const server = new ApolloServer({
  typeDefs,
  resolvers,
  validationRules: [
    depthLimit(5),  // 最大深さを5に制限
    createComplexityLimitRule(1000),  // 複雑度の上限
  ],
});

認証と認可

リゾルバレベルで認証・認可のチェックを行います。

const resolvers = {
  Query: {
    me: (_, __, context) => {
      // 認証チェック
      if (!context.user) {
        throw new GraphQLError('認証が必要です', {
          extensions: { code: 'UNAUTHENTICATED' },
        });
      }
      return context.user;
    },
  },
  Mutation: {
    deletePost: (_, { id }, context) => {
      // 認可チェック
      const post = getPost(id);
      if (post.authorId !== context.user.id) {
        throw new GraphQLError('権限がありません', {
          extensions: { code: 'FORBIDDEN' },
        });
      }
      return deletePost(id);
    },
  },
};

イントロスペクションの無効化

本番環境では、スキーマ情報を公開するイントロスペクション機能を無効化することを推奨します。

const server = new ApolloServer({
  typeDefs,
  resolvers,
  introspection: process.env.NODE_ENV !== 'production',
});

GraphQL導入の判断基準

すべてのプロジェクトにGraphQLが適しているわけではありません。導入を判断するための基準を紹介します。

GraphQLが適しているケース

  • 複数のクライアント(Web、モバイル、IoT)が同じバックエンドを利用する
  • フロントエンドが必要とするデータが頻繁に変わる
  • 複数のデータソースを統合したBFF(Backend for Frontend)が必要
  • リアルタイム通信(Subscription)が必要
  • フロントエンドチームの自律性を高めたい

REST APIが適しているケース

  • シンプルなCRUD操作が中心
  • HTTPキャッシュを最大限活用したい
  • ファイルアップロード・ダウンロードが主要な機能
  • チームにGraphQLの経験者がいない
  • 公開APIとして外部開発者に提供する(REST APIの方が広く理解されている)

まとめ

GraphQLは、データ取得の柔軟性と効率性を大幅に向上させるAPI技術です。本記事のポイントを振り返りましょう。

GraphQLの核心
クライアントが必要なデータの構造を宣言し、サーバーがそのとおりに返すという仕組みで、オーバーフェッチとアンダーフェッチを解決します。

スキーマ駆動開発
型システムに基づくスキーマがAPIの契約となり、フロントエンドとバックエンドの開発を並行して進められます。

N+1問題への対策
DataLoaderを使ったバッチ処理が、パフォーマンス問題を解決する標準的なアプローチです。

セキュリティの重要性
クエリの深さ制限、複雑度制限、認証・認可の実装は、本番運用に必須です。

適材適所の判断
GraphQLはすべてのケースに最適ではありません。プロジェクトの要件に応じて、REST APIと使い分けることが重要です。

まずはApollo Serverでシンプルなスキーマとリゾルバを実装し、GraphQL Playgroundで実際にクエリを実行してみることをおすすめします。RESTとの違いを体感することで、自プロジェクトへの適用判断がしやすくなるはずです。

#GraphQL#API#フロントエンド
共有:
無料メルマガ

週1回、最新の技術記事をお届け

AI・クラウド・開発の最新記事を毎週月曜にメールでお届けします。登録は無料、いつでも解除できます。

プライバシーポリシーに基づき管理します

起業準備に役立つ情報、もっとありますよ。

まずは話だけ聞いてもらう