graphql-design について
graphql-designは、GraphQLのスキーマやリゾルバーを設計する開発者向けのスキルです。ページネーション、変更ペイロード、DataLoader、サブスクリプションと安全対策を扱います。
主な機能
- 01Relay形式のカーソルページネーション
- 02結果とエラーを含む変更ペイロード
- 03リクエスト単位のDataLoaderによるN+1対策
- 04サブスクリプションのパターン
- 05クエリ深度と複雑度の制限
- 06モジュール型スキーマ設計
活用シーン
- 01リストフィールドのページネーション設計
- 02エンティティ検索の重複削減
- 03変更エラー構造の設計
- 04フィルター付きイベント購読の設計
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ rohitg00/ awesome-claude-code-toolkit --skill graphql-designインストール後、Claude Code で/graphql-designを入力して読み込まれたか確認します。
詳細
GraphQL 設計
スキーマ設計
type Query {
user(id: ID!): User
users(filter: UserFilter, first: Int = 20, after: String): UserConnection!
}
type Mutation {
createUser(input: CreateUserInput!): CreateUserPayload!
updateUser(id: ID!, input: UpdateUserInput!): UpdateUserPayload!
}
type Subscription {
orderStatusChanged(orderId: ID!): Order!
}
type User {
id: ID!
email: String!
name: String!
orders(first: Int = 10, after: String): OrderConnection!
createdAt: DateTime!
}
input CreateUserInput {
email: String!
name: String!
}
type CreateUserPayload {
user: User
errors: [UserError!]!
}
type UserError {
field: String!
message: String!
}
type UserConnection {
edges: [UserEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type UserEdge {
node: User!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
endCursor: String
}
ページネーションには Relay スタイルのコネクションを使用します。ミューテーションでは、結果とエラーの両方を含むペイロード型を返します。
リゾルバー
const resolvers: Resolvers = {
Query: {
user: async (_, { id }, ctx) => {
return ctx.dataloaders.user.load(id);
},
users: async (_, { filter, first, after }, ctx) => {
const cursor = after ? decodeCursor(after) : undefined;
const users = await ctx.db.user.findMany({
where: buildFilter(filter),
take: first + 1,
cursor: cursor ? { id: cursor } : undefined,
orderBy: { createdAt: "desc" },
});
const hasNextPage = users.length > first;
const edges = users.slice(0, first).map(user => ({
node: user,
cursor: encodeCursor(user.id),
}));
return {
edges,
pageInfo: {
hasNextPage,
endCursor: edges[edges.length - 1]?.cursor ?? null,
},
};
},
},
Mutation: {
createUser: async (_, { input }, ctx) => {
const existing = await ctx.db.user.findUnique({ where: { email: input.email } });
if (existing) {
return { user: null, errors: [{ field: "email", message: "Already taken" }] };
}
const user = await ctx.db.user.create({ data: input });
return { user, errors: [] };
},
},
User: {
orders: async (parent, { first, after }, ctx) => {
return ctx.dataloaders.userOrders.load({ userId: parent.id, first, after });
},
},
};
N+1 の防止に DataLoader を使用する
import DataLoader from "dataloader";
function createLoaders(db: Database) {
return {
user: new DataLoader<string, User>(async (ids) => {
const users = await db.user.findMany({ where: { id: { in: [...ids] } } });
const userMap = new Map(users.map(u => [u.id, u]));
return ids.map(id => userMap.get(id) ?? new Error(`User ${id} not found`));
}),
userOrders: new DataLoader<{ userId: string }, Order[]>(async (keys) => {
const userIds = keys.map(k => k.userId);
const orders = await db.order.findMany({
where: { userId: { in: userIds } },
orderBy: { createdAt: "desc" },
});
const grouped = new Map<string, Order[]>();
orders.forEach(o => {
const list = grouped.get(o.userId) ?? [];
list.push(o);
grouped.set(o.userId, list);
});
return keys.map(k => grouped.get(k.userId) ?? []);
}),
};
}
ユーザー間で古いキャッシュが共有されないよう、リクエストごとに新しい DataLoader インスタンスを作成します。
サブスクリプション
const pubsub = new PubSub();
const resolvers = {
Subscription: {
orderStatusChanged: {
subscribe: (_, { orderId }) => {
return pubsub.asyncIterableIterator(`ORDER_STATUS_${orderId}`);
},
},
},
Mutation: {
updateOrderStatus: async (_, { id, status }, ctx) => {
const order = await ctx.db.order.update({ where: { id }, data: { status } });
await pubsub.publish(`ORDER_STATUS_${id}`, { orderStatusChanged: order });
return { order, errors: [] };
},
},
};
アンチパターン
- データベーススキーマを GraphQL スキーマとしてそのまま公開する
- DataLoader を使わずにネストされたフィールドを解決する(N+1 クエリが発生します)
- 大規模なデータセットで、カーソルベースではなくオフセットベースのページネーションを使用する
- 型付きエラーペイロードを返さず、リゾルバーから生のエラーをスローする
- 型定義をモジュール化せず、単一の巨大なスキーマファイルを作成する
- 深度や複雑度の制限を設けず、無制限のクエリを許可する
チェックリスト
- すべてのリストフィールドで Relay スタイルのカーソルページネーションを使用する
- バッチ処理するすべてのエンティティ参照に DataLoader を使用する
- ミューテーションから、結果フィールドとエラーフィールドの両方を含むペイロード型を返す
- ミューテーションの引数に入力型を使用する
- クエリの深度と複雑度の制限を設定する
- コンテキスト内でリクエストごとに DataLoader インスタンスを作成する
- スキーマをドメイン固有のモジュールに分割する
- すべてのクライアントへのブロードキャストを避けるため、サブスクリプションでフィルター済みのトピックを使用する
コメント
Markdown 対応