TechHub

エンジニアの成長をサポートする技術情報サイト

← 記事一覧に戻る

GraphQLとは?RESTful APIとの違いと実装方法

公開日: 2024年2月13日 著者: mogura
GraphQLとは?RESTful APIとの違いと実装方法

疑問

GraphQLとは何で、RESTful APIとどのように違うのでしょうか?GraphQLの実装方法を一緒に学んでいきましょう。

導入

GraphQLは、Facebookが開発したクエリ言語とランタイムシステムです。RESTful APIと比較して、クライアントが必要なデータだけを効率的に取得できるという特徴があります。

本記事では、GraphQLの基本概念から、RESTful APIとの比較、スキーマ定義、リゾルバーの実装、Apollo Serverの使い方まで、実践的なコード例とともに詳しく解説していきます。

GraphQLのイメージ

解説

1. GraphQLとは

GraphQLは、APIのためのクエリ言語とランタイムシステムです。従来のRESTful APIとは異なり、クライアントが必要なデータを正確に指定して取得できるため、過剰なデータ取得や複数のエンドポイント呼び出しを避けることができます。

GraphQLの特徴

GraphQLには以下のような特徴があります:

  • 単一エンドポイント: 1つのエンドポイントで全ての操作(クエリ、ミューテーション、サブスクリプション)を実行できます。これにより、エンドポイントの管理が簡単になり、APIの構造がシンプルになります。
  • 必要なデータのみ取得: クライアントが必要なフィールドだけを指定できるため、ネットワーク転送量を削減し、パフォーマンスを向上させることができます。
  • 型安全: スキーマで型を定義することで、コンパイル時にエラーを検出でき、開発効率が向上します。
  • イントロスペクション: スキーマをクエリ可能なため、APIの構造を動的に取得し、開発ツールやドキュメント生成に活用できます。

2. RESTful APIとの比較

RESTful APIとGraphQLの主な違いを理解することで、適切なAPI設計ができるようになります。RESTful APIでは複数のエンドポイントを呼び出す必要があり、過剰なデータ取得が発生しやすいという問題があります。

RESTful APIとGraphQLを比較することで、それぞれの特徴と使い分けが理解できます。

RESTful APIの問題点

複数のエンドポイントを呼び出す必要があるため、ネットワークリクエストが増加し、レスポンス時間が長くなる可能性があります。また、エンドポイントが返すデータは固定されているため、不要なフィールドも含まれてしまいます。

GraphQLの解決策

クライアントが必要なフィールドを指定することで、過剰なデータ取得を防ぎ、ネットワーク転送量を削減できます。また、関連するデータを1つのクエリで取得できるため、リクエスト回数を減らすことができます。

RESTful APIの問題点

RESTful APIでは、ユーザー情報、投稿、フォロワーを取得するために3つのリクエストが必要です。また、ユーザー情報を取得する際に、不要なフィールドも含まれてしまいます。

// 複数のエンドポイントを呼び出す必要がある
GET /api/users/123
GET /api/users/123/posts
GET /api/users/123/followers

// 過剰なデータ取得(不要なフィールドも含まれる)
GET /api/users/123
// レスポンス: { id, name, email, phone, address, ... } // 全てのフィールドが返る

GraphQLの解決策

GraphQLでは、1つのクエリでユーザー情報、投稿、フォロワーを取得できます。また、必要なフィールドだけを指定できるため、不要なデータは取得されません。

# 1つのクエリで必要なデータのみ取得
query {
  user(id: 123) {
    name
    posts {
      title
      createdAt
    }
    followers {
      name
    }
  }
}

3. GraphQLスキーマの定義

GraphQLスキーマは、APIの構造と型を定義する重要な要素です。スキーマを先に設計することで、型安全なAPIを構築でき、クライアントとサーバー間の契約を明確にできます。

基本的なスキーマ

型定義では、各オブジェクトのフィールドとその型を指定します。`!`は必須フィールド、`[]`は配列を表します。Queryはデータの読み取り、Mutationはデータの変更を定義します。

スキーマでは、型(Type)、クエリ(Query)、ミューテーション(Mutation)を定義します。

基本的なスキーマ

このスキーマでは、User型とPost型を定義し、それぞれの関係を表現しています。Query型ではデータの読み取り、Mutation型ではデータの作成・更新・削除を定義しています。

# schema.graphql
type User {
  id: ID!
  name: String!
  email: String!
  posts: [Post!]!
}

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

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

type Mutation {
  createUser(name: String!, email: String!): User!
  updateUser(id: ID!, name: String, email: String): User!
  deleteUser(id: ID!): Boolean!
}

4. Apollo Serverの実装

Apollo Serverは、GraphQLサーバーを簡単に構築できるNode.jsフレームワークです。スキーマ定義とリゾルバーを実装するだけで、本番環境で使用できるGraphQL APIを構築できます。

Apollo Serverを使用してGraphQLサーバーを実装する方法を説明します。

基本的なセットアップ

Apollo Serverは、スキーマ定義(typeDefs)とリゾルバー(resolvers)を設定することで動作します。リゾルバーは、クエリやミューテーションが実行された際に呼び出される関数です。

Apollo Serverをインストールし、基本的なサーバーを起動します。

基本的なセットアップ

この例では、Apollo Serverを起動し、ユーザー一覧と特定のユーザーを取得するクエリを実装しています。リゾルバー関数は、クエリが実行された際に呼び出され、データを返します。

// npm install apollo-server graphql
const { ApolloServer, gql } = require('apollo-server');

// スキーマ定義
const typeDefs = gql`
  type User {
    id: ID!
    name: String!
    email: String!
  }
  
  type Query {
    users: [User!]!
    user(id: ID!): User
  }
`;

// リゾルバー
const resolvers = {
  Query: {
    users: () => users,
    user: (parent, args) => users.find(u => u.id === args.id)
  }
};

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

server.listen().then(({ url }) => {
  console.log(`Server ready at ${url}`);
});

5. リゾルバーの実装

リゾルバーは、GraphQLクエリやミューテーションが実行された際に呼び出される関数です。リゾルバーでデータベースからデータを取得したり、ビジネスロジックを実行したりします。

基本的なリゾルバー

Queryリゾルバーはデータの読み取り、Mutationリゾルバーはデータの変更を実装します。また、型のフィールドに対してリゾルバーを定義することで、関連データを取得できます。

リゾルバーは、Query、Mutation、各型のフィールドに対して定義できます。

基本的なリゾルバー

この例では、Queryリゾルバーでユーザー一覧と特定のユーザーを取得し、User型のpostsフィールドでユーザーの投稿を取得しています。Mutationリゾルバーでは、ユーザーの作成・更新・削除を実装しています。

const resolvers = {
  Query: {
    users: async () => {
      // データベースから取得
      return await User.find();
    },
    user: async (parent, args) => {
      return await User.findById(args.id);
    }
  },
  
  User: {
    posts: async (parent) => {
      // ユーザーの投稿を取得
      return await Post.find({ authorId: parent.id });
    }
  },
  
  Mutation: {
    createUser: async (parent, args) => {
      const user = new User({
        name: args.name,
        email: args.email
      });
      return await user.save();
    },
    
    updateUser: async (parent, args) => {
      return await User.findByIdAndUpdate(
        args.id,
        { name: args.name, email: args.email },
        { new: true }
      );
    },
    
    deleteUser: async (parent, args) => {
      await User.findByIdAndDelete(args.id);
      return true;
    }
  }
};

6. クエリの実行

GraphQLクエリは、クライアントが必要なデータを指定して取得するための構文です。変数を使用することで、動的なクエリを実行できます。

GraphQLクエリの基本的な書き方と、変数の使用方法を説明します。

基本的なクエリ

クエリでは、必要なフィールドだけを指定することで、過剰なデータ取得を防げます。また、ネストしたフィールドを指定することで、関連データも取得できます。

変数の使用

変数を使用することで、同じクエリ構造で異なる値を取得できます。変数は型を指定する必要があり、クエリの再利用性が向上します。

基本的なクエリ

最初のクエリでは、ユーザー一覧を取得しています。2つ目のクエリでは、特定のユーザーとその投稿を取得しています。

# ユーザー一覧を取得
query {
  users {
    id
    name
    email
  }
}

# 特定のユーザーを取得
query {
  user(id: "123") {
    id
    name
    email
    posts {
      id
      title
    }
  }
}

変数の使用

この例では、変数$userIdを使用してユーザーを取得しています。変数の型はID!(必須)として定義されています。

query GetUser($userId: ID!) {
  user(id: $userId) {
    id
    name
    email
  }
}

# 変数
{
  "userId": "123"
}

7. ミューテーション

ミューテーションは、データの作成・更新・削除を行うためのGraphQL操作です。RESTful APIのPOST、PUT、DELETEに相当しますが、GraphQLでは1つのエンドポイントで全ての操作を実行できます。

ミューテーションを使用して、データの作成・更新・削除を行う方法を説明します。

データの作成・更新・削除

ミューテーションは、データを変更する操作を定義します。作成、更新、削除の各操作に対して、適切なパラメータと戻り値を定義します。

データの作成・更新・削除

これらのミューテーションでは、ユーザーの作成、更新、削除を実行しています。各ミューテーションは、必要なパラメータを受け取り、変更後のデータを返します。

# ユーザー作成
mutation CreateUser($name: String!, $email: String!) {
  createUser(name: $name, email: $email) {
    id
    name
    email
  }
}

# ユーザー更新
mutation UpdateUser($id: ID!, $name: String) {
  updateUser(id: $id, name: $name) {
    id
    name
    email
  }
}

# ユーザー削除
mutation DeleteUser($id: ID!) {
  deleteUser(id: $id)
}

8. 認証と認可

GraphQL APIでは、認証と認可を適切に実装することが重要です。コンテキストを使用して認証情報を管理し、リゾルバーで認可チェックを行います。

GraphQL APIで認証と認可を実装する方法を説明します。

コンテキストの使用

コンテキストは、リクエストごとに作成され、リゾルバーに渡されます。コンテキストに認証情報を含めることで、リゾルバーで認証チェックを行えます。

コンテキストの使用

この例では、リクエストヘッダーからトークンを取得し、コンテキストにユーザー情報を設定しています。リゾルバーでは、コンテキストのユーザー情報を確認して認証チェックを行います。

const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: ({ req }) => {
    // トークンからユーザー情報を取得
    const token = req.headers.authorization?.split(' ')[1];
    const user = token ? verifyToken(token) : null;
    return { user };
  }
});

// リゾルバーで認証チェック
const resolvers = {
  Query: {
    profile: (parent, args, context) => {
      if (!context.user) {
        throw new Error('Authentication required');
      }
      return context.user;
    }
  }
};

9. エラーハンドリング

GraphQL APIでは、適切なエラーハンドリングが重要です。カスタムエラーを使用することで、クライアントに分かりやすいエラーメッセージを返すことができます。

GraphQL APIでエラーハンドリングを実装する方法を説明します。

カスタムエラー

GraphQLでは、標準的なエラーコードを使用することで、クライアントがエラーを適切に処理できます。認証エラー、バリデーションエラーなど、エラーの種類に応じて適切なエラーコードを返します。

カスタムエラー

この例では、認証エラーとバリデーションエラーのカスタムクラスを定義しています。リゾルバーでは、適切なエラーをスローすることで、クライアントに分かりやすいエラーメッセージを返します。

class AuthenticationError extends Error {
  constructor(message) {
    super(message);
    this.extensions = {
      code: 'UNAUTHENTICATED'
    };
  }
}

class ValidationError extends Error {
  constructor(message, field) {
    super(message);
    this.extensions = {
      code: 'BAD_USER_INPUT',
      field
    };
  }
}

// リゾルバーでの使用
const resolvers = {
  Mutation: {
    createUser: async (parent, args) => {
      if (!args.email || !args.email.includes('@')) {
        throw new ValidationError('Invalid email', 'email');
      }
      // ...
    }
  }
};

10. データローダー

データローダーは、N+1問題を解決するための仕組みです。複数のリクエストをバッチ処理することで、データベースへのアクセス回数を削減し、パフォーマンスを向上させます。

データローダーを使用して、N+1問題を解決する方法を説明します。

N+1問題の解決

N+1問題は、1つのクエリでN件のデータを取得した後、各データに対して追加のクエリを実行することで発生します。データローダーを使用することで、複数のリクエストを1つのバッチリクエストにまとめることができます。

N+1問題の解決

この例では、DataLoaderを使用してユーザー情報をバッチ取得しています。複数の投稿の著者情報を取得する際、個別のクエリではなく1つのバッチクエリで取得できます。

// npm install dataloader
const DataLoader = require('dataloader');

// バッチローダー
const userLoader = new DataLoader(async (userIds) => {
  const users = await User.find({ _id: { $in: userIds } });
  return userIds.map(id => users.find(u => u.id === id));
});

// リゾルバーで使用
const resolvers = {
  Post: {
    author: async (parent) => {
      return await userLoader.load(parent.authorId);
    }
  }
};

11. サブスクリプション

サブスクリプションは、リアルタイムでデータの更新を受け取るためのGraphQL機能です。WebSocketを使用して、サーバーからクライアントへデータをプッシュできます。

サブスクリプションを使用して、リアルタイム更新を実装する方法を説明します。

リアルタイム更新

サブスクリプションは、PubSubパターンを使用して実装されます。データが変更された際にイベントを発行し、サブスクライブしているクライアントに通知します。

サブスクリプションの定義

スキーマでサブスクリプションを定義します。

type Subscription {
  postCreated: Post!
  userUpdated(userId: ID!): User!
}

サブスクリプションの実装

この例では、投稿が作成された際にイベントを発行し、サブスクライブしているクライアントに通知しています。

const { PubSub } = require('apollo-server');
const pubsub = new PubSub();

const resolvers = {
  Subscription: {
    postCreated: {
      subscribe: () => pubsub.asyncIterator(['POST_CREATED'])
    }
  },
  
  Mutation: {
    createPost: async (parent, args) => {
      const post = await Post.create(args);
      pubsub.publish('POST_CREATED', { postCreated: post });
      return post;
    }
  }
};

12. ベストプラクティス

GraphQL APIを本番環境で使用する際は、いくつかのベストプラクティスに従うことで、パフォーマンスと保守性を向上させることができます。

GraphQL APIのベストプラクティスを説明します。

  • スキーマファースト: スキーマを先に設計することで、型安全なAPIを構築でき、クライアントとサーバー間の契約を明確にできます。
  • リゾルバーの最適化: データローダーを使用してN+1問題を解決し、パフォーマンスを向上させます。
  • エラーハンドリング: 適切なエラーメッセージとコードを返すことで、クライアントがエラーを適切に処理できます。
  • 認証・認可: コンテキストで認証情報を管理し、リゾルバーで認可チェックを行います。
  • バリデーション: 入力データの検証を行い、不正なデータを防ぎます。
  • パフォーマンス: クエリの複雑さを考慮し、過剰に複雑なクエリを防ぎます。

まとめ

GraphQLは、クライアントが必要なデータを正確に指定して取得できるクエリ言語です。RESTful APIと比較して、過剰なデータ取得を防ぎ、複数のエンドポイントを呼び出す必要がありません。

スキーマ定義、リゾルバーの実装、認証・認可、エラーハンドリングを適切に実装することで、本番環境で使用できるGraphQL APIを構築できます。データローダーを使用してN+1問題を解決し、パフォーマンスを最適化することも重要です。

実践的なプロジェクトでGraphQLを導入し、経験を積むことで、より効率的で柔軟なAPIを構築できるようになります。

バックエンドの認証・認可とは?JWTとOAuth2.0の実装方法 Node.jsとExpress.jsでRESTful APIを構築する方法