graphql-api-development について
graphql-api-developmentは、graphql-jsでGraphQL APIを作る開発者向けの学習スキルです。スキーマ設計、リゾルバー、リアルタイムサブスクリプション、セキュリティ、性能、テスト、デプロイを扱います。
主な機能
- 01スキーマと型システムの設計
- 02クエリ、ミューテーション、リゾルバー
- 03リアルタイムサブスクリプション
- 04認証と認可のパターン
- 05キャッシュとクエリ最適化
- 06APIのテストとデプロイの実践
活用シーン
- 01異なるデータ要件を持つクライアント向けAPIの構築
- 02ネストしたデータ関係の提供
- 03サブスクリプションによるライブ更新の追加
- 04高コストまたは深くネストしたクエリの制限
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ manutej/ luxor-claude-marketplace --skill graphql-api-developmentインストール後、Claude Code で/graphql-api-developmentを入力して読み込まれたか確認します。
同梱ファイルEXAMPLES.mdREADME.md
詳細
GraphQL API の開発
graphql-js を使用して、本番環境に対応した GraphQL API を構築するための包括的なスキルです。スキーマ設計、型システム、リゾルバー、クエリ、ミューテーション、サブスクリプション、認証、認可、キャッシュ、テスト、デプロイ戦略を習得しましょう。
このスキルを使う場面
このスキルは、次のような場合に使用します。
- Web クライアントやモバイルクライアント向けに、柔軟なデータ取得が必要な新しい API を構築する場合
- REST API を、より効率的なデータアクセスパターンで置き換える、または拡張する場合
- 複雑でネストされたデータ関係を持つアプリケーションの API を開発する場合
- 異なるデータニーズを持つ複数のクライアントタイプ(Web、モバイル、デスクトップ)にデータを提供する API を作成する場合
- サブスクリプションとライブ更新が必要なリアルタイムアプリケーションを構築する場合
- クライアントが必要なデータを正確に指定できる API を設計する場合
- Node.js と Express を使用して GraphQL サーバーを開発する場合
- 強力なスキーマ検証を備えた型安全な API を実装する場合
- イントロスペクション機能を組み込んだ、自己文書化 API を作成する場合
- 統一された API に構成する必要があるマイクロサービスを構築する場合
GraphQL が REST より優れている場面
GraphQL の利点
- 正確なデータ取得:クライアントは必要なデータだけをリクエストでき、過剰取得や取得不足がありません
- 単一リクエスト:複数の REST エンドポイントを呼び出す代わりに、1 回の往復で複数のリソースを取得できます
- 強い型付け:スキーマで型を正確に定義でき、検証やツールの利用が可能になります
- イントロスペクション:クエリ可能なスキーマを備えた、自己文書化 API です
- バージョン管理が不要:既存のクエリを壊さずに新しいフィールドを追加できます
- リアルタイム更新:ライブデータ向けのサブスクリプションを標準でサポートします
- ネストされたリソース:N+1 クエリを発生させず、複雑な関係を自然に扱えます
- クライアント主導:クライアントがデータの形状を制御できるため、バックエンドの変更が減ります
REST を使い続ける場面
- 標準的なリソースに対するシンプルな CRUD 操作
- ファイルのアップロード/ダウンロード(GraphQL ではマルチパート処理が必要です)
- HTTP キャッシュが重要な場合(GraphQL は通常 POST を使用します)
- チームが GraphQL に不慣れな場合(習得に時間がかかります)
- 既存の REST インフラストラクチャが適切に機能している場合
基本概念
GraphQL の型システム
GraphQL の型システムは、その基盤です。すべての GraphQL API では、次のものを定義します。
- スカラー型:基本的なデータ型(String、Int、Float、Boolean、ID)
- オブジェクト型:フィールドを持つ複合型
- クエリ型:読み取り操作のエントリーポイント
- ミューテーション型:書き込み操作のエントリーポイント
- サブスクリプション型:リアルタイム更新のエントリーポイント
- 入力型:ミューテーション用の複合入力
- 列挙型:値の固定セット
- インターフェース:オブジェクトが実装する抽象型
- ユニオン:複数の型のいずれかになり得る型
- 非 null 型:null にできない型
- リスト型:型の配列
スキーマ定義
GraphQL スキーマを定義する方法は 2 つあります。
1. スキーマ定義言語(SDL) - 宣言的で読みやすい形式:
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
content: String
author: User!
}
type Query {
user(id: ID!): User
posts: [Post!]!
}
2. プログラムによる API - 型安全なプログラム形式:
const UserType = new GraphQLObjectType({
name: 'User',
fields: {
id: { type: new GraphQLNonNull(GraphQLID) },
name: { type: new GraphQLNonNull(GraphQLString) },
email: { type: new GraphQLNonNull(GraphQLString) },
posts: { type: new GraphQLList(new GraphQLNonNull(PostType)) }
}
});
リゾルバー
リゾルバーは、スキーマのフィールドに対応するデータを返す関数です。すべてのフィールドにリゾルバーを設定できます。
const resolvers = {
Query: {
user: (parent, args, context, info) => {
return context.db.findUserById(args.id);
}
},
User: {
posts: (user, args, context) => {
return context.db.findPostsByAuthorId(user.id);
}
}
};
リゾルバー関数のシグネチャ:
parent:親リゾルバーの結果args:フィールドに渡される引数context:共有コンテキスト(データベース、認証など)info:フィールド固有のメタデータ
クエリ
クエリは API からデータを取得します。
query GetUser {
user(id: "123") {
id
name
email
posts {
title
content
}
}
}
ミューテーション
ミューテーションはデータを変更します。
mutation CreatePost {
createPost(input: {
title: "GraphQL is awesome"
content: "Here's why..."
authorId: "123"
}) {
id
title
author {
name
}
}
}
サブスクリプション
サブスクリプションはリアルタイム更新を可能にします。
subscription OnPostCreated {
postCreated {
id
title
author {
name
}
}
}
スキーマ設計パターン
パターン 1:ミューテーション用の入力型
複雑なミューテーション引数には、必ず入力型を使用します。
input CreateUserInput {
name: String!
email: String!
age: Int
bio: String
}
type Mutation {
createUser(input: CreateUserInput!): User!
}
理由:拡張しやすく、整理しやすく、複数のミューテーションで再利用できます。
パターン 2:共通フィールド用のインターフェース
複数の型でフィールドを共有する場合は、インターフェースを使用します。
interface Node {
id: ID!
createdAt: String!
updatedAt: String!
}
type User implements Node {
id: ID!
createdAt: String!
updatedAt: String!
name: String!
email: String!
}
type Post implements Node {
id: ID!
createdAt: String!
updatedAt: String!
title: String!
content: String
}
パターン 3:多態的な戻り値用のユニオン
フィールドが異なる型を返す場合は、ユニオンを使用します。
union SearchResult = User | Post | Comment
type Query {
search(query: String!): [SearchResult!]!
}
パターン 4:ページネーションのパターン
オフセットベースのページネーション:
type Query {
posts(offset: Int, limit: Int): PostConnection!
}
type PostConnection {
items: [Post!]!
total: Int!
hasMore: Boolean!
}
カーソルベースのページネーション(Relay 形式):
type Query {
posts(first: Int, after: String): PostConnection!
}
type PostConnection {
edges: [PostEdge!]!
pageInfo: PageInfo!
}
type PostEdge {
node: Post!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
endCursor: String
}
パターン 5:エラー処理
フィールドレベルのエラー:
type MutationPayload {
success: Boolean!
message: String
user: User
errors: [Error!]
}
type Error {
field: String!
message: String!
}
ユニオンベースのエラー処理:
union CreateUserResult = User | ValidationError | DatabaseError
type ValidationError {
field: String!
message: String!
}
パターン 6:ディレクティブによるバージョン管理
バージョンを変更する代わりに、フィールドを非推奨にします:
type User {
name: String! @deprecated(reason: "Use firstName and lastName")
firstName: String!
lastName: String!
}
クエリの最適化とパフォーマンス
N+1 問題
問題:ネストされたデータを取得すると、データベースへのクエリが複数回発生します:
// BAD: N+1 queries
const UserType = new GraphQLObjectType({
name: 'User',
fields: {
posts: {
type: new GraphQLList(PostType),
resolve: (user) => {
// This runs once PER user!
return db.getPostsByUserId(user.id);
}
}
}
});
// Query for 100 users = 1 query for users + 100 queries for posts = 101 queries
DataLoader による解決
DataLoader はリクエストをまとめて処理し、キャッシュします:
import DataLoader from 'dataloader';
// Create DataLoader
const postLoader = new DataLoader(async (userIds) => {
// Single query for all user IDs
const posts = await db.getPostsByUserIds(userIds);
// Group posts by userId
const postsByUserId = {};
posts.forEach(post => {
if (!postsByUserId[post.authorId]) {
postsByUserId[post.authorId] = [];
}
postsByUserId[post.authorId].push(post);
});
// Return in same order as userIds
return userIds.map(id => postsByUserId[id] || []);
});
// Use in resolver
const UserType = new GraphQLObjectType({
name: 'User',
fields: {
posts: {
type: new GraphQLList(PostType),
resolve: (user, args, context) => {
return context.loaders.postLoader.load(user.id);
}
}
}
});
// Add to context
const context = {
loaders: {
postLoader: new DataLoader(batchLoadPosts)
}
};
クエリの複雑度分析
負荷の高いクエリを制限します:
import { getComplexity, simpleEstimator } from 'graphql-query-complexity';
const complexity = getComplexity({
schema,
query,
estimators: [
simpleEstimator({ defaultComplexity: 1 })
]
});
if (complexity > 1000) {
throw new Error('Query too complex');
}
深度制限
深くネストされたクエリを防ぎます:
import depthLimit from 'graphql-depth-limit';
const server = new ApolloServer({
schema,
validationRules: [depthLimit(5)]
});
ミューテーションと入力の検証
ミューテーション設計パターン
input CreatePostInput {
title: String!
content: String!
authorId: ID!
tags: [String!]
}
type CreatePostPayload {
post: Post
errors: [UserError!]
success: Boolean!
}
type UserError {
message: String!
field: String
}
type Mutation {
createPost(input: CreatePostInput!): CreatePostPayload!
}
入力の検証
const Mutation = new GraphQLObjectType({
name: 'Mutation',
fields: {
createPost: {
type: CreatePostPayload,
args: {
input: { type: new GraphQLNonNull(CreatePostInput) }
},
resolve: async (_, { input }, context) => {
// Validate input
const errors = [];
if (input.title.length < 3) {
errors.push({
field: 'title',
message: 'Title must be at least 3 characters'
});
}
if (input.content.length < 10) {
errors.push({
field: 'content',
message: 'Content must be at least 10 characters'
});
}
if (errors.length > 0) {
return { errors, success: false, post: null };
}
// Create post
const post = await context.db.createPost(input);
return { post, errors: [], success: true };
}
}
}
});
サブスクリプションとリアルタイム更新
サブスクリプションの設定
import { GraphQLObjectType, GraphQLString } from 'graphql';
import { PubSub } from 'graphql-subscriptions';
const pubsub = new PubSub();
const Subscription = new GraphQLObjectType({
name: 'Subscription',
fields: {
postCreated: {
type: PostType,
subscribe: () => pubsub.asyncIterator(['POST_CREATED'])
},
messageReceived: {
type: MessageType,
args: {
channelId: { type: new GraphQLNonNull(GraphQLID) }
},
subscribe: (_, { channelId }) => {
return pubsub.asyncIterator([`MESSAGE_${channelId}`]);
}
}
}
});
イベントの発行
const Mutation = new GraphQLObjectType({
name: 'Mutation',
fields: {
createPost: {
type: PostType,
args: {
input: { type: new GraphQLNonNull(CreatePostInput) }
},
resolve: async (_, { input }, context) => {
const post = await context.db.createPost(input);
// Publish to subscribers
pubsub.publish('POST_CREATED', { postCreated: post });
return post;
}
}
}
});
WebSocket サーバーの設定
import { createServer } from 'http';
import { WebSocketServer } from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import { execute, subscribe } from 'graphql';
import express from 'express';
const app = express();
const httpServer = createServer(app);
// WebSocket server for subscriptions
const wsServer = new WebSocketServer({
server: httpServer,
path: '/graphql'
});
useServer(
{
schema,
execute,
subscribe,
context: (ctx) => {
// Access connection params, headers
return {
userId: ctx.connectionParams?.userId,
db: database
};
}
},
wsServer
);
httpServer.listen(4000);
認証と認可
コンテキストベースの認証
import jwt from 'jsonwebtoken';
// Middleware to extract user
const authMiddleware = async (req) => {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return { user: null };
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const user = await db.findUserById(decoded.userId);
return { user };
} catch (error) {
return { user: null };
}
};
// Add to GraphQL context
app.all('/graphql', async (req, res) => {
const auth = await authMiddleware(req);
createHandler({
schema,
context: {
user: auth.user,
db: database
}
})(req, res);
});
リゾルバーレベルの認可
const Query = new GraphQLObjectType({
name: 'Query',
fields: {
me: {
type: UserType,
resolve: (_, __, context) => {
if (!context.user) {
throw new Error('Authentication required');
}
return context.user;
}
},
adminData: {
type: GraphQLString,
resolve: (_, __, context) => {
if (!context.user) {
throw new Error('Authentication required');
}
if (context.user.role !== 'admin') {
throw new Error('Admin access required');
}
return 'Secret admin data';
}
}
}
});
フィールドレベルの認可
const PostType = new GraphQLObjectType({
name: 'Post',
fields: {
title: { type: GraphQLString },
content: { type: GraphQLString },
draft: {
type: GraphQLBoolean,
resolve: (post, args, context) => {
// Only author can see draft status
if (post.authorId !== context.user?.id) {
return null;
}
return post.draft;
}
}
}
});
ディレクティブベースの認可
directive @auth(requires: Role = USER) on FIELD_DEFINITION
enum Role {
USER
ADMIN
MODERATOR
}
type Query {
publicData: String
userData: String @auth(requires: USER)
adminData: String @auth(requires: ADMIN)
}
import { mapSchema, getDirective, MapperKind } from '@graphql-tools/utils';
function authDirective(schema, directiveName) {
return mapSchema(schema, {
[MapperKind.OBJECT_FIELD]: (fieldConfig) => {
const authDirective = getDirective(schema, fieldConfig, directiveName)?.[0];
if (authDirective) {
const { requires } = authDirective;
const { resolve = defaultFieldResolver } = fieldConfig;
fieldConfig.resolve = async (source, args, context, info) => {
if (!context.user) {
throw new Error('Authentication required');
}
if (context.user.role !== requires) {
throw new Error(`${requires} role required`);
}
return resolve(source, args, context, info);
};
}
return fieldConfig;
}
});
}
キャッシュ戦略
インメモリーキャッシュ
import { LRUCache } from 'lru-cache';
const cache = new LRUCache({
max: 500,
ttl: 1000 * 60 * 5 // 5 minutes
});
const Query = new GraphQLObjectType({
name: 'Query',
fields: {
product: {
type: ProductType,
args: { id: { type: new GraphQLNonNull(GraphQLID) } },
resolve: async (_, { id }, context) => {
const cacheKey = `product:${id}`;
const cached = cache.get(cacheKey);
if (cached) {
return cached;
}
const product = await context.db.findProductById(id);
cache.set(cacheKey, product);
return product;
}
}
}
});
Redis キャッシュ
import Redis from 'ioredis';
const redis = new Redis({
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT
});
const Query = new GraphQLObjectType({
name: 'Query',
fields: {
user: {
type: UserType,
args: { id: { type: new GraphQLNonNull(GraphQLID) } },
resolve: async (_, { id }, context) => {
const cacheKey = `user:${id}`;
// Check cache
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// Fetch from database
const user = await context.db.findUserById(id);
// Cache for 10 minutes
await redis.setex(cacheKey, 600, JSON.stringify(user));
return user;
}
}
}
});
キャッシュの無効化
const Mutation = new GraphQLObjectType({
name: 'Mutation',
fields: {
updateUser: {
type: UserType,
args: {
id: { type: new GraphQLNonNull(GraphQLID) },
input: { type: new GraphQLNonNull(UpdateUserInput) }
},
resolve: async (_, { id, input }, context) => {
const user = await context.db.updateUser(id, input);
// Invalidate cache
const cacheKey = `user:${id}`;
await redis.del(cacheKey);
// Also invalidate list caches
await redis.del('users:all');
return user;
}
}
}
});
エラー処理
カスタムエラークラス
class AuthenticationError extends Error {
constructor(message) {
super(message);
this.name = 'AuthenticationError';
this.extensions = { code: 'UNAUTHENTICATED' };
}
}
class ForbiddenError extends Error {
constructor(message) {
super(message);
this.name = 'ForbiddenError';
this.extensions = { code: 'FORBIDDEN' };
}
}
class ValidationError extends Error {
constructor(message, fields) {
super(message);
this.name = 'ValidationError';
this.extensions = {
code: 'BAD_USER_INPUT',
fields
};
}
}
エラーの整形
import { formatError } from 'graphql';
const customFormatError = (error) => {
// Log error for monitoring
console.error('GraphQL Error:', {
message: error.message,
locations: error.locations,
path: error.path,
extensions: error.extensions
});
// Don't expose internal errors to clients
if (error.message.startsWith('Database')) {
return {
message: 'Internal server error',
extensions: { code: 'INTERNAL_SERVER_ERROR' }
};
}
return formatError(error);
};
const server = new ApolloServer({
schema,
formatError: customFormatError
});
適切なエラーレスポンス
const Query = new GraphQLObjectType({
name: 'Query',
fields: {
user: {
type: UserType,
args: { id: { type: new GraphQLNonNull(GraphQLID) } },
resolve: async (_, { id }, context) => {
try {
const user = await context.db.findUserById(id);
if (!user) {
throw new Error(`User with ID ${id} not found`);
}
return user;
} catch (error) {
// Log error
console.error('Error fetching user:', error);
// Re-throw with user-friendly message
if (error.code === 'ECONNREFUSED') {
throw new Error('Unable to connect to database');
}
throw error;
}
}
}
}
});
GraphQL API のテスト
リゾルバーの単体テスト
import { describe, it, expect, jest } from '@jest/globals';
describe('User resolver', () => {
it('returns user by ID', async () => {
const mockDb = {
findUserById: jest.fn().mockResolvedValue({
id: '1',
name: 'Alice',
email: '[email protected]'
})
};
const context = { db: mockDb };
const result = await userResolver.resolve(null, { id: '1' }, context);
expect(mockDb.findUserById).toHaveBeenCalledWith('1');
expect(result).toEqual({
id: '1',
name: 'Alice',
email: '[email protected]'
});
});
it('throws error for non-existent user', async () => {
const mockDb = {
findUserById: jest.fn().mockResolvedValue(null)
};
const context = { db: mockDb };
await expect(
userResolver.resolve(null, { id: '999' }, context)
).rejects.toThrow('User with ID 999 not found');
});
});
統合テスト
import { graphql } from 'graphql';
import { schema } from './schema';
describe('GraphQL Schema', () => {
it('executes user query', async () => {
const query = `
query {
user(id: "1") {
id
name
email
}
}
`;
const result = await graphql({
schema,
source: query,
contextValue: {
db: mockDatabase,
user: null
}
});
expect(result.errors).toBeUndefined();
expect(result.data?.user).toEqual({
id: '1',
name: 'Alice',
email: '[email protected]'
});
});
it('handles authentication errors', async () => {
const query = `
query {
me {
id
name
}
}
`;
const result = await graphql({
schema,
source: query,
contextValue: {
db: mockDatabase,
user: null
}
});
expect(result.errors).toBeDefined();
expect(result.errors[0].message).toBe('Authentication required');
});
});
Apollo Server を使ったテスト
import { ApolloServer } from '@apollo/server';
const testServer = new ApolloServer({
schema,
});
describe('User queries', () => {
it('fetches user successfully', async () => {
const response = await testServer.executeOperation({
query: `
query GetUser($id: ID!) {
user(id: $id) {
id
name
}
}
`,
variables: { id: '1' }
});
expect(response.body.singleResult.errors).toBeUndefined();
expect(response.body.singleResult.data?.user).toMatchObject({
id: '1',
name: expect.any(String)
});
});
});
本番環境でのベストプラクティス
スキーマの構成
src/
├── schema/
│ ├── index.js # Combine all types
│ ├── types/
│ │ ├── user.js # User type and resolvers
│ │ ├── post.js # Post type and resolvers
│ │ └── comment.js # Comment type and resolvers
│ ├── queries/
│ │ ├── user.js # User queries
│ │ └── post.js # Post queries
│ ├── mutations/
│ │ ├── user.js # User mutations
│ │ └── post.js # Post mutations
│ └── subscriptions/
│ └── post.js # Post subscriptions
├── directives/
│ └── auth.js # Authorization directive
├── utils/
│ ├── loaders.js # DataLoader instances
│ └── context.js # Context builder
└── server.js # Server setup
監視とログ記録
import { ApolloServerPluginLandingPageGraphQLPlayground } from '@apollo/server-plugin-landing-page-graphql-playground';
const server = new ApolloServer({
schema,
plugins: [
// Request logging
{
async requestDidStart(requestContext) {
console.log('Request started:', requestContext.request.query);
return {
async didEncounterErrors(ctx) {
console.error('Errors:', ctx.errors);
},
async willSendResponse(ctx) {
console.log('Response sent');
}
};
}
},
// Performance monitoring
{
async requestDidStart() {
const start = Date.now();
return {
async willSendResponse() {
const duration = Date.now() - start;
console.log(`Request duration: ${duration}ms`);
}
};
}
}
]
});
レート制限
import rateLimit from 'express-rate-limit';
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per window
message: 'Too many requests, please try again later'
});
app.use('/graphql', limiter);
クエリのホワイトリスト登録
const allowedQueries = new Set([
'query GetUser { user(id: $id) { id name email } }',
'mutation CreatePost { createPost(input: $input) { id title } }'
]);
const validateQuery = (query) => {
const normalized = query.replace(/\s+/g, ' ').trim();
if (!allowedQueries.has(normalized)) {
throw new Error('Query not whitelisted');
}
};
セキュリティヘッダー
import helmet from 'helmet';
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"],
}
},
crossOriginEmbedderPolicy: false
}));
高度なパターン
フェデレーション(マイクロサービス)
import { buildSubgraphSchema } from '@apollo/subgraph';
// Users service
const userSchema = buildSubgraphSchema({
typeDefs: `
type User @key(fields: "id") {
id: ID!
name: String!
email: String!
}
`,
resolvers: {
User: {
__resolveReference(user) {
return findUserById(user.id);
}
}
}
});
// Posts service
const postSchema = buildSubgraphSchema({
typeDefs: `
type Post {
id: ID!
title: String!
author: User!
}
extend type User @key(fields: "id") {
id: ID! @external
posts: [Post!]!
}
`,
resolvers: {
Post: {
author(post) {
return { __typename: 'User', id: post.authorId };
}
},
User: {
posts(user) {
return findPostsByAuthorId(user.id);
}
}
}
});
カスタムスカラー
import { GraphQLScalarType, Kind } from 'graphql';
const DateTimeScalar = new GraphQLScalarType({
name: 'DateTime',
description: 'ISO-8601 DateTime string',
serialize(value) {
// Send to client
return value instanceof Date ? value.toISOString() : null;
},
parseValue(value) {
// From variables
return new Date(value);
},
parseLiteral(ast) {
// From query string
if (ast.kind === Kind.STRING) {
return new Date(ast.value);
}
return null;
}
});
// Use in schema
const schema = new GraphQLSchema({
types: [DateTimeScalar],
query: new GraphQLObjectType({
name: 'Query',
fields: {
now: {
type: DateTimeScalar,
resolve: () => new Date()
}
}
})
});
バッチ操作
const Mutation = new GraphQLObjectType({
name: 'Mutation',
fields: {
batchCreateUsers: {
type: new GraphQLList(UserType),
args: {
inputs: {
type: new GraphQLNonNull(
new GraphQLList(new GraphQLNonNull(CreateUserInput))
)
}
},
resolve: async (_, { inputs }, context) => {
const users = await Promise.all(
inputs.map(input => context.db.createUser(input))
);
return users;
}
}
}
});
よく使われるパターンのまとめ
- 入力型を使用する:複数の引数を持つすべてのミューテーションに使用します
- DataLoader を実装する:ネストされたデータの N+1 クエリを解決します
- ページネーションを追加する:際限なく増加する可能性のあるリストフィールドに追加します
- エラーを適切に処理する:ユーザーにわかりやすいエラーメッセージを返します
- 入力を検証する:データベース操作の前に、リゾルバーレベルで検証します
- 共有状態にコンテキストを使用する:データベース、認証、ローダーに使用します
- 認可を実装する:リゾルバーまたはディレクティブのレベルで実装します
- 積極的にキャッシュする:頻繁にアクセスされるデータには Redis またはインメモリーキャッシュを使用します
- パフォーマンスを監視する:クエリの複雑度と実行時間を追跡します
- @deprecated でバージョン管理する:既存のクエリを決して破壊しません
- 徹底的にテストする:リゾルバーを単体テストし、クエリを統合テストします
- スキーマをドキュメント化する:SDL で説明を使用します
- 非 null を適切に使う:本当に必須のフィールドにのみ使用します
- スキーマを整理する:ドメインごとにモジュールへ分割します
- 本番環境を保護する:レート制限、クエリのホワイトリスト登録、深度制限を行います
リソースとツール
必須ライブラリ
- graphql-js:GraphQL のコア実装
- express:Web サーバーフレームワーク
- graphql-http:GraphQL 用の HTTP ハンドラー
- dataloader:バッチ処理とキャッシュ
- graphql-ws:サブスクリプション用の WebSocket サーバー
- graphql-scalars:一般的なカスタムスカラー
- graphql-tools:スキーマ操作ユーティリティ
開発ツール
- GraphiQL:ブラウザー内で使える GraphQL IDE
- GraphQL Playground:高度な GraphQL IDE
- Apollo Studio:スキーマレジストリと監視
- GraphQL Code Generator:TypeScript 型を生成
- eslint-plugin-graphql:GraphQL クエリを lint
学習リソース
- GraphQL 公式ドキュメント:https://graphql.org
- GraphQL.js リポジトリ:https://github.com/graphql/graphql-js
- GraphQL の学び方:https://howtographql.com
- Apollo GraphQL:https://apollographql.com
- GraphQL Weekly ニュースレター:https://graphqlweekly.com
スキルのバージョン:1.0.0 最終更新日:2025 年 10 月 スキルのカテゴリ:API 開発、バックエンド、GraphQL、Web 開発 対応環境:Node.js、Express、TypeScript、JavaScript