Skip to content
Published on

GraphQL完全ガイド2025:RESTを超えて — スキーマ設計からFederation、パフォーマンス最適化まで

Authors

はじめに:なぜGraphQLなのか?

2015年にFacebookがオープンソースとして公開(こうかい)したGraphQLは、今やAPI設計(せっけい)の標準的(ひょうじゅんてき)な選択肢(せんたくし)となりました。GitHub、Shopify、Netflix、Airbnbなど主要(しゅよう)テック企業(きぎょう)が採用(さいよう)し、RESTの限界(げんかい)を超(こ)える新(あたら)しいパラダイムを示(しめ)しています。

この記事(きじ)では、GraphQLの基礎(きそ)からFederationを活用(かつよう)したマイクロサービス統合(とうごう)、パフォーマンス最適化(さいてきか)まで、実務(じつむ)ですぐに活用(かつよう)できる内容(ないよう)を扱(あつか)います。


1. REST vs GraphQL:根本的(こんぽんてき)な違(ちが)い

RESTの限界(げんかい)

REST APIは長年(ながねん)WebAPIの標準(ひょうじゅん)でしたが、いくつかの根本的(こんぽんてき)な問題(もんだい)があります。

Over-fetching:不要(ふよう)なデータまでレスポンスに含(ふく)まれます。ユーザー名(めい)だけ必要(ひつよう)なのに、プロフィール全体(ぜんたい)が返(かえ)ってきます。

Under-fetching:一(ひと)つの画面(がめん)を描画(びょうが)するために複数(ふくすう)のエンドポイントを呼(よ)び出(だ)す必要(ひつよう)があります。ユーザー情報(じょうほう) + 投稿(とうこう) + コメント = 3回(かい)のリクエスト。

エンドポイント管理(かんり):クライアントの要件(ようけん)が変(か)わるたびに新(あたら)しいエンドポイントを作成(さくせい)するか、既存(きそん)のものを修正(しゅうせい)する必要(ひつよう)があります。

GraphQLの解決策(かいけつさく)

# 1回のリクエストで必要なデータだけを正確に取得
query GetUserWithPosts {
  user(id: "123") {
    name
    email
    posts(first: 5) {
      title
      commentCount
    }
  }
}
区分RESTGraphQL
エンドポイントリソースごとに別URL単一エンドポイント
データ量サーバーが決定クライアントが決定
バージョン管理v1, v2 URL分岐スキーマ進化(deprecation)
型システムSwagger/OpenAPIで補強内蔵型システム(SDL)
リアルタイム別途WebSocket実装Subscription内蔵
キャッシュHTTPキャッシュ活用が容易別途戦略が必要
学習曲線低い中〜高い

核心的(かくしんてき)な違(ちが)い:宣言的(せんげんてき)データリクエスト

RESTは**どこから(WHERE)データを取得(しゅとく)するかを指定(してい)し、GraphQLは何(なに)を(WHAT)**取得(しゅとく)するかを宣言(せんげん)します。


2. スキーマ設計(せっけい) (Schema Definition Language)

基本(きほん)型(かた)システム

GraphQLのスキーマはAPIの契約書(けいやくしょ)です。SDLを使(つか)って型(かた)を定義(ていぎ)します。

# スカラー型: String, Int, Float, Boolean, ID
# ! はNon-nullを意味します

type User {
  id: ID!
  name: String!
  email: String!
  age: Int
  posts: [Post!]!
  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!
}

Query, Mutation, Subscription

GraphQLには3つのオペレーションタイプがあります。

type Query {
  # データ取得
  user(id: ID!): User
  users(filter: UserFilter, pagination: PaginationInput): UserConnection!
  post(id: ID!): Post
  searchPosts(query: String!): [Post!]!
}

type Mutation {
  # データ変更
  createUser(input: CreateUserInput!): User!
  updateUser(id: ID!, input: UpdateUserInput!): User!
  deleteUser(id: ID!): Boolean!
  createPost(input: CreatePostInput!): Post!
  likePost(postId: ID!): Post!
}

type Subscription {
  # リアルタイム更新
  postCreated: Post!
  commentAdded(postId: ID!): Comment!
  userStatusChanged(userId: ID!): User!
}

Input型(かた)とEnum

input CreateUserInput {
  name: String!
  email: String!
  age: Int
  role: UserRole = USER
}

input UserFilter {
  role: UserRole
  nameContains: String
  createdAfter: DateTime
}

input PaginationInput {
  first: Int = 20
  after: String
}

enum UserRole {
  ADMIN
  EDITOR
  USER
  GUEST
}

enum PostStatus {
  DRAFT
  PUBLISHED
  ARCHIVED
}

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
}

3. Resolverパターン

基本(きほん)Resolver構造(こうぞう)

Resolverはスキーマの各(かく)フィールドを実際(じっさい)のデータと接続(せつぞく)します。

// Resolverの4つの引数: parent, args, context, info
const resolvers = {
  Query: {
    user: async (parent, args, context, info) => {
      const { id } = args;
      const { dataSources, currentUser } = context;
      return dataSources.userAPI.getUser(id);
    },

    users: async (_, { filter, pagination }, { dataSources }) => {
      return dataSources.userAPI.getUsers(filter, pagination);
    },

    searchPosts: async (_, { query }, { dataSources }) => {
      return dataSources.postAPI.search(query);
    },
  },

  Mutation: {
    createUser: async (_, { input }, { dataSources, currentUser }) => {
      if (!currentUser?.isAdmin) {
        throw new ForbiddenError('Admin access required');
      }
      return dataSources.userAPI.createUser(input);
    },

    likePost: async (_, { postId }, { dataSources, currentUser }) => {
      if (!currentUser) {
        throw new AuthenticationError('Login required');
      }
      return dataSources.postAPI.likePost(postId, currentUser.id);
    },
  },

  // 型Resolver - リレーションフィールドの解決
  User: {
    posts: async (user, _, { dataSources }) => {
      return dataSources.postAPI.getPostsByAuthor(user.id);
    },
  },

  Post: {
    author: async (post, _, { dataSources }) => {
      return dataSources.userAPI.getUser(post.authorId);
    },
    comments: async (post, _, { dataSources }) => {
      return dataSources.commentAPI.getByPost(post.id);
    },
  },
};

Context設定(せってい)

import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@apollo/server/express4';

const server = new ApolloServer({
  typeDefs,
  resolvers,
});

await server.start();

app.use(
  '/graphql',
  expressMiddleware(server, {
    context: async ({ req }) => {
      const token = req.headers.authorization || '';
      const currentUser = await getUserFromToken(token);
      return {
        currentUser,
        dataSources: {
          userAPI: new UserDataSource(),
          postAPI: new PostDataSource(),
          commentAPI: new CommentDataSource(),
        },
      };
    },
  })
);

4. N+1問題(もんだい)とDataLoader

N+1問題(もんだい)とは?

投稿(とうこう)リストを取得(しゅとく)した後(あと)、各(かく)投稿(とうこう)の著者(ちょしゃ)を個別(こべつ)クエリで取得(しゅとく)するとN+1問題(もんだい)が発生(はっせい)します。

Query: posts(first: 10)1回のクエリ(投稿10件)
  Post.author (post 1)2回目のクエリ
  Post.author (post 2)3回目のクエリ
  ...
  Post.author (post 10)11回目のクエリ

合計(ごうけい)11回(かい)のDBクエリが実行(じっこう)されます。

DataLoaderで解決(かいけつ)

DataLoaderは同(おな)じイベントループティック内(ない)のリクエストをまとめてバッチ処理(しょり)します。

import DataLoader from 'dataloader';

// バッチ関数: ID配列を受け取り、同じ順序で結果を返す
const userLoader = new DataLoader(async (userIds: string[]) => {
  const users = await db.users.findMany({
    where: { id: { in: userIds } },
  });

  // リクエストされたID順に結果をマッピング
  const userMap = new Map(users.map((u) => [u.id, u]));
  return userIds.map((id) => userMap.get(id) || null);
});

// Resolverで使用
const resolvers = {
  Post: {
    author: (post, _, { loaders }) => {
      return loaders.userLoader.load(post.authorId);
    },
  },
};

結果(けっか):11回(かい)のクエリが2回(かい)に削減(さくげん)されます。

Query: posts(first: 10)1回のクエリ
  DataLoader batch: users([1,2,3,...,10])2回のクエリ(バッチ)

DataLoaderコンテキストパターン

// リクエストごとに新しいDataLoaderインスタンスを生成
function createLoaders() {
  return {
    userLoader: new DataLoader((ids) => batchGetUsers(ids)),
    postLoader: new DataLoader((ids) => batchGetPosts(ids)),
    commentLoader: new DataLoader((ids) => batchGetComments(ids)),
    // 1:Nリレーション用loader
    postsByAuthorLoader: new DataLoader(async (authorIds) => {
      const posts = await db.posts.findMany({
        where: { authorId: { in: authorIds } },
      });
      const grouped = groupBy(posts, 'authorId');
      return authorIds.map((id) => grouped[id] || []);
    }),
  };
}

// contextでリクエストごとに新規生成
context: async ({ req }) => ({
  currentUser: await getUser(req),
  loaders: createLoaders(),
}),

5. Apollo ServerとClient

Apollo Serverセットアップ

import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { makeExecutableSchema } from '@graphql-tools/schema';

const schema = makeExecutableSchema({
  typeDefs,
  resolvers,
});

const server = new ApolloServer({
  schema,
  plugins: [
    // 本番環境でイントロスペクション無効化
    ApolloServerPluginLandingPageDisabled(),
    // 使用量レポーティング
    ApolloServerPluginUsageReporting(),
  ],
  formatError: (formattedError, error) => {
    // エラーフォーマット - 内部情報を隠す
    if (formattedError.extensions?.code === 'INTERNAL_SERVER_ERROR') {
      return { message: 'Internal server error' };
    }
    return formattedError;
  },
});

const { url } = await startStandaloneServer(server, {
  listen: { port: 4000 },
  context: async ({ req }) => ({
    currentUser: await authenticateUser(req),
    loaders: createLoaders(),
  }),
});

console.log(`Server ready at ${url}`);

Apollo Client (React)

import {
  ApolloClient,
  InMemoryCache,
  ApolloProvider,
  useQuery,
  useMutation,
  gql,
} from '@apollo/client';

const client = new ApolloClient({
  uri: 'https://api.example.com/graphql',
  cache: new InMemoryCache({
    typePolicies: {
      Query: {
        fields: {
          posts: {
            // Relayスタイルカーソルページネーションマージ
            keyArgs: ['filter'],
            merge(existing, incoming, { args }) {
              if (!args?.after) return incoming;
              return {
                ...incoming,
                edges: [...(existing?.edges || []), ...incoming.edges],
              };
            },
          },
        },
      },
    },
  }),
  defaultOptions: {
    watchQuery: {
      fetchPolicy: 'cache-and-network',
    },
  },
});

// Reactコンポーネントでの使用
const GET_POSTS = gql`
  query GetPosts($first: Int!, $after: String) {
    posts(first: $first, after: $after) {
      edges {
        node {
          id
          title
          author {
            name
          }
        }
      }
      pageInfo {
        hasNextPage
        endCursor
      }
    }
  }
`;

function PostList() {
  const { data, loading, error, fetchMore } = useQuery(GET_POSTS, {
    variables: { first: 10 },
  });

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error occurred</p>;

  return (
    <div>
      {data.posts.edges.map(({ node }) => (
        <PostCard key={node.id} post={node} />
      ))}
      {data.posts.pageInfo.hasNextPage && (
        <button
          onClick={() =>
            fetchMore({
              variables: { after: data.posts.pageInfo.endCursor },
            })
          }
        >
          Load More
        </button>
      )}
    </div>
  );
}

6. Apollo Federation v2

Federationとは?

Apollo Federationは複数(ふくすう)のGraphQLサービス(Subgraph)を一(ひと)つの統合(とうごう)API(Supergraph)に合成(ごうせい)します。各(かく)チームが独立(どくりつ)してサービスを開発(かいはつ)しながら、クライアントには一(ひと)つのGraphQLエンドポイントを提供(ていきょう)します。

Subgraph設計(せっけい)

# --- Users Subgraph ---
type User @key(fields: "id") {
  id: ID!
  name: String!
  email: String!
  role: UserRole!
}

type Query {
  user(id: ID!): User
  me: User
}

# --- Posts Subgraph ---
type Post @key(fields: "id") {
  id: ID!
  title: String!
  content: String!
  author: User!
  publishedAt: DateTime
}

# 他のsubgraphのUser型を拡張
type User @key(fields: "id") {
  id: ID!
  posts: [Post!]!        # Postsサービスが追加するフィールド
  postCount: Int!
}

type Query {
  post(id: ID!): Post
  feed(first: Int = 20, after: String): PostConnection!
}

# --- Reviews Subgraph ---
type Review @key(fields: "id") {
  id: ID!
  rating: Int!
  comment: String
  author: User!
  post: Post!
}

type Post @key(fields: "id") {
  id: ID!
  reviews: [Review!]!     # Reviewsサービスが追加
  averageRating: Float
}

Router設定(せってい)(Apollo Router)

# router.yaml
supergraph:
  listen: 0.0.0.0:4000

subgraphs:
  users:
    routing_url: http://users-service:4001/graphql
  posts:
    routing_url: http://posts-service:4002/graphql
  reviews:
    routing_url: http://reviews-service:4003/graphql

headers:
  all:
    request:
      - propagate:
          named: authorization

SubgraphのReference Resolver

// Posts Subgraph
const resolvers = {
  User: {
    // FederationがUserをresolveする際に呼び出される
    __resolveReference: async (user, { loaders }) => {
      // user.idのみが渡される
      return loaders.postsByUserLoader.load(user.id);
    },
    posts: async (user, _, { dataSources }) => {
      return dataSources.postAPI.getByAuthor(user.id);
    },
    postCount: async (user, _, { dataSources }) => {
      return dataSources.postAPI.countByAuthor(user.id);
    },
  },
};

7. Subscription(リアルタイム機能(きのう))

サーバーセットアップ

import { createServer } from 'http';
import { WebSocketServer } from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import { PubSub } from 'graphql-subscriptions';

const pubsub = new PubSub();

const httpServer = createServer(app);
const wsServer = new WebSocketServer({
  server: httpServer,
  path: '/graphql',
});

useServer(
  {
    schema,
    context: async (ctx) => {
      const token = ctx.connectionParams?.authorization;
      const user = await authenticateUser(token);
      return { currentUser: user, pubsub };
    },
  },
  wsServer
);

// Resolver
const resolvers = {
  Subscription: {
    commentAdded: {
      subscribe: (_, { postId }) => {
        return pubsub.asyncIterator(`COMMENT_ADDED_${postId}`);
      },
    },
    postCreated: {
      subscribe: () => pubsub.asyncIterator('POST_CREATED'),
    },
  },

  Mutation: {
    createComment: async (_, { input }, { pubsub, currentUser }) => {
      const comment = await db.comments.create({
        data: { ...input, authorId: currentUser.id },
      });

      // 購読者に通知
      pubsub.publish(`COMMENT_ADDED_${input.postId}`, {
        commentAdded: comment,
      });

      return comment;
    },
  },
};

クライアントSubscription

import { useSubscription, gql } from '@apollo/client';

const COMMENT_SUBSCRIPTION = gql`
  subscription OnCommentAdded($postId: ID!) {
    commentAdded(postId: $postId) {
      id
      body
      author {
        name
      }
      createdAt
    }
  }
`;

function CommentFeed({ postId }) {
  const { data, loading } = useSubscription(COMMENT_SUBSCRIPTION, {
    variables: { postId },
  });

  // 新しいコメントが来るたびにdataが自動更新される
  if (data?.commentAdded) {
    return <NewComment comment={data.commentAdded} />;
  }

  return null;
}

8. 認証(にんしょう)(Authentication)と認可(にんか)(Authorization)

Directiveベースの認可(にんか)

# カスタムdirective定義
directive @auth(requires: Role = USER) on FIELD_DEFINITION | OBJECT

enum Role {
  ADMIN
  EDITOR
  USER
  GUEST
}

type Query {
  publicPosts: [Post!]!
  me: User! @auth
  adminDashboard: Dashboard! @auth(requires: ADMIN)
  userManagement: [User!]! @auth(requires: ADMIN)
}

type Mutation {
  createPost(input: CreatePostInput!): Post! @auth
  deletePost(id: ID!): Boolean! @auth(requires: ADMIN)
  updateProfile(input: UpdateProfileInput!): User! @auth
}

Directive実装(じっそう)

import { mapSchema, getDirective, MapperKind } from '@graphql-tools/utils';
import { defaultFieldResolver } from 'graphql';

function authDirectiveTransformer(schema) {
  return mapSchema(schema, {
    [MapperKind.OBJECT_FIELD]: (fieldConfig) => {
      const authDirective = getDirective(schema, fieldConfig, 'auth')?.[0];

      if (authDirective) {
        const requiredRole = authDirective.requires || 'USER';
        const originalResolve = fieldConfig.resolve || defaultFieldResolver;

        fieldConfig.resolve = async (source, args, context, info) => {
          const { currentUser } = context;

          if (!currentUser) {
            throw new AuthenticationError('Not authenticated');
          }

          if (!hasRole(currentUser, requiredRole)) {
            throw new ForbiddenError('Insufficient permissions');
          }

          return originalResolve(source, args, context, info);
        };
      }

      return fieldConfig;
    },
  });
}

9. パフォーマンス最適化(さいてきか)

Query Complexity制限(せいげん)

import { createComplexityLimitRule } from 'graphql-validation-complexity';

const server = new ApolloServer({
  schema,
  validationRules: [
    createComplexityLimitRule(1000, {
      scalarCost: 1,
      objectCost: 2,
      listFactor: 10,
      formatErrorMessage: (cost) =>
        `Query too complex: cost ${cost} exceeds maximum 1000`,
    }),
  ],
});

Depth Limiting

import depthLimit from 'graphql-depth-limit';

const server = new ApolloServer({
  schema,
  validationRules: [depthLimit(7)],
});

Persisted Queries (APQ)

// サーバー
import { ApolloServerPluginCacheControl } from '@apollo/server/plugin/cacheControl';

const server = new ApolloServer({
  schema,
  plugins: [ApolloServerPluginCacheControl({ defaultMaxAge: 60 })],
  persistedQueries: {
    cache: new KeyValueCache(), // Redisなど
  },
});

// クライアント
import { createPersistedQueryLink } from '@apollo/client/link/persisted-queries';
import { sha256 } from 'crypto-hash';

const link = createPersistedQueryLink({ sha256 });

const client = new ApolloClient({
  link: link.concat(httpLink),
  cache: new InMemoryCache(),
});

キャッシュ制御(せいぎょ)

type Query {
  posts: [Post!]! @cacheControl(maxAge: 300)
  user(id: ID!): User @cacheControl(maxAge: 60)
  me: User @cacheControl(maxAge: 0, scope: PRIVATE)
}

type Post @cacheControl(maxAge: 600) {
  id: ID!
  title: String!
  author: User! @cacheControl(inheritMaxAge: true)
  viewCount: Int! @cacheControl(maxAge: 30)
}

10. キャッシュ戦略(せんりゃく)

サーバーサイドキャッシュ

import { KeyvAdapter } from '@apollo/utils.keyvadapter';
import Keyv from 'keyv';
import KeyvRedis from '@keyv/redis';

const server = new ApolloServer({
  schema,
  cache: new KeyvAdapter(new Keyv({ store: new KeyvRedis('redis://localhost:6379') })),
});

// DataSourceレベルのキャッシュ
class CachedUserAPI {
  private cache: RedisClient;
  private ttl = 300; // 5分

  async getUser(id: string): Promise<User> {
    const cacheKey = `user:${id}`;
    const cached = await this.cache.get(cacheKey);
    if (cached) return JSON.parse(cached);

    const user = await db.users.findUnique({ where: { id } });
    await this.cache.set(cacheKey, JSON.stringify(user), 'EX', this.ttl);
    return user;
  }
}

クライアントサイドキャッシュ(Apollo Cache)

const cache = new InMemoryCache({
  typePolicies: {
    User: {
      keyFields: ['id'],
      fields: {
        fullName: {
          read(_, { readField }) {
            const first = readField('firstName');
            const last = readField('lastName');
            return `${first} ${last}`;
          },
        },
      },
    },
    Post: {
      keyFields: ['id'],
    },
    Query: {
      fields: {
        post(_, { args, toReference }) {
          return toReference({ __typename: 'Post', id: args?.id });
        },
      },
    },
  },
});

11. テスト

Resolver単体(たんたい)テスト

import { describe, it, expect, vi } from 'vitest';

describe('User Resolvers', () => {
  it('should return user by id', async () => {
    const mockUser = { id: '1', name: 'Test User', email: 'test@example.com' };
    const context = {
      dataSources: {
        userAPI: { getUser: vi.fn().mockResolvedValue(mockUser) },
      },
    };

    const result = await resolvers.Query.user(null, { id: '1' }, context, null);
    expect(result).toEqual(mockUser);
    expect(context.dataSources.userAPI.getUser).toHaveBeenCalledWith('1');
  });

  it('should throw if not admin for createUser', async () => {
    const context = {
      currentUser: { role: 'USER' },
      dataSources: { userAPI: { createUser: vi.fn() } },
    };

    await expect(
      resolvers.Mutation.createUser(null, { input: {} }, context, null)
    ).rejects.toThrow('Admin access required');
  });
});

統合(とうごう)テスト

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

describe('GraphQL Integration', () => {
  let server: ApolloServer;

  beforeAll(async () => {
    server = new ApolloServer({ typeDefs, resolvers });
  });

  it('should execute GetUser query', async () => {
    const response = await server.executeOperation({
      query: `
        query GetUser($id: ID!) {
          user(id: $id) {
            id
            name
            email
          }
        }
      `,
      variables: { id: '1' },
    });

    expect(response.body.kind).toBe('single');
    expect(response.body.singleResult.errors).toBeUndefined();
    expect(response.body.singleResult.data?.user).toBeDefined();
  });
});

12. GraphQLを使(つか)うべきでないとき

すべての状況(じょうきょう)にGraphQLが適切(てきせつ)なわけではありません。

RESTが適(てき)している場合(ばあい):

  • シンプルなCRUD API(リソース構造(こうぞう)が明確(めいかく)な場合(ばあい))
  • ファイルアップロード/ダウンロードが主機能(しゅきのう)のサービス
  • HTTPキャッシュが重要(じゅうよう)な公開(こうかい)API
  • チーム規模(きぼ)が小(ちい)さく学習(がくしゅう)コストを負担(ふたん)できない場合(ばあい)
  • サーバー間(かん)内部(ないぶ)通信(つうしん)(gRPCがより適切(てきせつ))

GraphQLが輝(かがや)く場合(ばあい):

  • 多様(たよう)なクライアント(Web、モバイル、TV)が同(おな)じAPIを使用(しよう)
  • フロントエンドチームが高速(こうそく)にイテレーションする必要(ひつよう)がある場合(ばあい)
  • データ関係(かんけい)が複雑(ふくざつ)なドメイン(SNS、ECサイト)
  • マイクロサービスを統合(とうごう)APIとして提供(ていきょう)する必要(ひつよう)がある場合(ばあい)
  • リアルタイム機能(きのう)が必要(ひつよう)なアプリケーション

13. 実務(じつむ)面接(めんせつ)質問(しつもん)15選(せん)

基礎(きそ)(1-5)

Q1. GraphQLとRESTの核心的(かくしんてき)な違(ちが)いを説明(せつめい)してください。

GraphQLはクライアントが必要(ひつよう)なデータの構造(こうぞう)を直接(ちょくせつ)指定(してい)するクエリ言語(げんご)です。RESTがサーバー定義(ていぎ)のエンドポイントから固定(こてい)レスポンスを返(かえ)すのに対(たい)し、GraphQLは単一(たんいつ)エンドポイントからクライアントが希望(きぼう)するフィールドのみを選択的(せんたくてき)にリクエストします。

Q2. Over-fetchingとUnder-fetchingを説明(せつめい)してください。

Over-fetchingはAPIがクライアントに必要(ひつよう)以上(いじょう)のデータを返(かえ)すことで、Under-fetchingは必要(ひつよう)なデータを得(え)るために複数回(ふくすうかい)のAPI呼(よ)び出(だ)しが必要(ひつよう)なことです。

Q3. SDL(Schema Definition Language)の主要(しゅよう)な型(かた)を説明(せつめい)してください。

Object型(かた)(カスタムフィールド定義(ていぎ))、Scalar型(かた)(String, Int, Float, Boolean, ID)、Enum(列挙型(れっきょがた))、Input(入力用(にゅうりょくよう)オブジェクト)、Interface(共通(きょうつう)フィールド定義(ていぎ))、Union(複数型(ふくすうがた)のいずれか)があります。

Q4. Query、Mutation、Subscriptionの違(ちが)いを説明(せつめい)してください。

Queryはデータ取得(しゅとく)(GETに類似(るいじ))、Mutationはデータ変更(へんこう)(POST/PUT/DELETEに類似(るいじ))、SubscriptionはWebSocketを通(つう)じたリアルタイムイベントストリームです。

Q5. Non-null(!)とリスト型(かた)の組(く)み合(あ)わせを説明(せつめい)してください。

[String]はnull許容(きょよう)文字列(もじれつ)のnull許容(きょよう)配列(はいれつ)、[String!]はnull不可(ふか)文字列(もじれつ)のnull許容(きょよう)配列(はいれつ)、[String!]!はnull不可(ふか)文字列(もじれつ)のnull不可(ふか)配列(はいれつ)です。

中級(ちゅうきゅう)(6-10)

Q6. N+1問題(もんだい)とは何(なに)で、どう解決(かいけつ)しますか?

親(おや)リストを取得(しゅとく)した後(あと)、各(かく)項目(こうもく)のリレーションデータを個別(こべつ)クエリで取得(しゅとく)する問題(もんだい)です。DataLoaderで同(おな)じティック内(ない)のリクエストをまとめて1回(かい)のバッチクエリで実行(じっこう)します。

Q7. Apollo Federationの核心(かくしん)コンセプトを説明(せつめい)してください。

複数(ふくすう)のSubgraphをRouterが一(ひと)つのSupergraphに統合(とうごう)します。@keyでエンティティを識別(しきべつ)し、__resolveReferenceで他(た)のサービスのエンティティをresolveします。

Q8. GraphQLで認証(にんしょう)と認可(にんか)をどう実装(じっそう)しますか?

認証(にんしょう)はcontextでトークンを検証(けんしょう)してcurrentUserを設定(せってい)します。認可(にんか)はカスタムdirective(@auth)やresolver内(ない)でロールベースの検査(けんさ)を行(おこな)います。

Q9. Persisted Queriesの利点(りてん)は何(なに)ですか?

クエリ文字列(もじれつ)の代(か)わりにハッシュ値(ち)を送信(そうしん)してネットワークペイロードを削減(さくげん)し、サーバーで許可(きょか)されたクエリのみ実行(じっこう)してセキュリティを強化(きょうか)します。

Q10. CursorベースとOffsetベースのPaginationの違(ちが)いは?

Offsetはページ番号(ばんごう)ベースで、挿入(そうにゅう)/削除(さくじょ)時(じ)にデータの重複(じゅうふく)/欠落(けつらく)が発生(はっせい)します。Cursorは特定(とくてい)位置(いち)(opaque cursor)ベースで、リアルタイムデータでも安定(あんてい)しています。

上級(じょうきゅう)(11-15)

Q11. Query ComplexityとDepth Limitを説明(せつめい)してください。

Query Complexityは各(かく)フィールドにコストを付与(ふよ)し、総(そう)コストが上限(じょうげん)を超(こ)えると拒否(きょひ)します。Depth Limitはネスト深度(しんど)を制限(せいげん)して悪意(あくい)のある再帰(さいき)クエリを防止(ぼうし)します。

Q12. FederationにおけるEntityとReference Resolverの動作(どうさ)を説明(せつめい)してください。

Entityは@keyで識別(しきべつ)される型(かた)です。Routerが他(た)のsubgraphのデータが必要(ひつよう)な場合(ばあい)、__resolveReferenceを呼(よ)び出(だ)してkeyフィールドだけで全(ぜん)エンティティをresolveします。

Q13. GraphQLのキャッシュがRESTより難(むずか)しい理由(りゆう)は?

RESTはURLベースのHTTPキャッシュを直接(ちょくせつ)活用(かつよう)できますが、GraphQLは単一(たんいつ)エンドポイントにPOSTリクエストを使用(しよう)するためHTTPキャッシュが使(つか)えません。APQ、CDNキャッシュキー設定(せってい)、正規化(せいきか)されたクライアントキャッシュなど別途(べっと)の戦略(せんりゃく)が必要(ひつよう)です。

Q14. Subscriptionのプロダクションスケーリングはどうしますか?

インメモリPubSubの代(か)わりにRedis PubSubやKafkaを使用(しよう)します。複数(ふくすう)のサーバーインスタンス間(かん)でメッセージを共有(きょうゆう)し、WebSocket接続(せつぞく)のロードバランシングにはsticky sessionや専用(せんよう)WebSocketゲートウェイを構成(こうせい)します。

Q15. スキーマ進化(しんか)(Evolution)戦略(せんりゃく)を説明(せつめい)してください。

@deprecated directiveでフィールドの廃止(はいし)を予告(よこく)し、新(あたら)しいフィールドを追加(ついか)します。フィールド削除前(さくじょまえ)にクライアント使用量(しようりょう)をモニタリングし、スキーマレジストリで変更(へんこう)の互換性(ごかんせい)を検証(けんしょう)します。RESTのURLバージョン管理(かんり)より柔軟(じゅうなん)です。


14. 実践(じっせん)クイズ5問(もん)

Q1. GraphQL Queryで「必ずnullでない文字列の配列」を表す型はどれですか?

正解(せいかい):[String!]!

[String!]! — 配列(はいれつ)自体(じたい)もnullではなく(!)、配列(はいれつ)内(ない)の各(かく)要素(ようそ)もnullではありません(String!)。[String]は配列(はいれつ)もnull可能(かのう)、要素(ようそ)もnull可能(かのう)です。

Q2. DataLoaderの核心的な動作原理を説明してください。

正解(せいかい):同(おな)じイベントループティック内(ない)の.load()呼(よ)び出(だ)しをまとめてバッチ関数(かんすう)を1回(かい)実行(じっこう)します。

DataLoaderはリクエストをキューに蓄(たくわ)え、現在(げんざい)のティックが終(お)わるとすべてのIDを一(ひと)つのバッチ関数(かんすう)に渡(わた)します。これによりN+1クエリを1+1クエリに削減(さくげん)します。

Q3. Apollo Federationにおける@key directiveの役割は?

正解(せいかい):エンティティ(Entity)を一意(いちい)に識別(しきべつ)するフィールドを指定(してい)します。

@key(fields: "id")はその型(かた)がFederationのエンティティであることを宣言(せんげん)し、Routerが他(た)のsubgraphからこのエンティティを参照(さんしょう)する際(さい)にidフィールドを使(つか)ってresolveできるようにします。

Q4. GraphQL Subscriptionが内部的に使用するプロトコルは?

正解(せいかい):WebSocket

SubscriptionはHTTPリクエスト-レスポンスではなく、WebSocketを通(つう)じた双方向(そうほうこう)接続(せつぞく)を使用(しよう)します。graphql-wsプロトコルでサーバーからクライアントにリアルタイムイベントをpushします。

Q5. Persisted Queriesのセキュリティ上の利点を説明してください。

正解(せいかい):サーバーに登録(とうろく)されたクエリのみ実行(じっこう)し、任意(にんい)のクエリ(悪意(あくい)のあるクエリ)の実行(じっこう)を防止(ぼうし)します。

APQ(Automatic Persisted Queries)はクエリのSHA256ハッシュを送信(そうしん)します。サーバーはハッシュとマッピングされたクエリのみ実行(じっこう)するため、攻撃者(こうげきしゃ)が任意(にんい)の複雑(ふくざつ)なクエリを送(おく)ってサーバーリソースを消費(しょうひ)することを防(ふせ)ぎます。


参考(さんこう)資料(しりょう)(References)

  1. GraphQL公式ドキュメント
  2. Apollo GraphQLドキュメント
  3. Apollo Federationガイド
  4. GraphQL Spec (2021)
  5. DataLoader GitHub
  6. graphql-wsプロトコル
  7. How to GraphQL - チュートリアル
  8. Relay公式ドキュメント
  9. GraphQL Best Practices
  10. Production-Ready GraphQL (Marc-Andre Giroux)
  11. The Guild - GraphQL Tools
  12. GraphQL Security (OWASP)
  13. Apollo Client Reactガイド
  14. Netflix: GraphQL at Scale
  15. Shopify: GraphQL Design Tutorial
  16. GitHub GraphQL API