𝕏 f B! L
案件・求人数 12,345
案件を探す(準備中) エージェントを探す(準備中) お役立ち情報 ログイン
案件・求人数 12,345
OpenAI Codex CLIでGraphQL APIを開発する方法|スキーマ設計から実装まで

OpenAI Codex CLIでGraphQL APIを開発する方法|スキーマ設計から実装まで

OpenAI Codex CLIGraphQLAPI開発スキーマ設計バックエンド
目次
⚡ 3秒でわかる!この記事のポイント
  • Codex CLIにドメインモデルを伝えるだけで、GraphQLスキーマとリゾルバを自動生成できる
  • DataLoaderパターンによるN+1対策やJWT認証の実装もAI支援で効率化
  • GraphQL案件はSES市場で単価65-85万円と高く、フロントエンド連携スキルとして需要増加中

「REST APIをGraphQLに移行したいけど、スキーマ設計から始めるのが大変…」

GraphQL APIの開発は、スキーマ定義・リゾルバ実装・データローダー設定・認証/認可と、やるべきことが多いのが現実です。OpenAI Codex CLIを活用すれば、ドメインモデルの理解からコード生成まで、開発プロセス全体を効率化できます。

この記事では、Codex CLIを使ったGraphQL API開発の実践ワークフローを体系的に解説します。

この記事でわかること
  • Codex CLIでGraphQLスキーマを自動設計する方法
  • リゾルバの自動生成とカスタマイズテクニック
  • DataLoaderによるN+1問題の解決
  • JWT認証・RBAC認可の実装パターン
  • SES現場でのGraphQL案件の需要と必要スキル

GraphQL × Codex CLIの基本ワークフロー

なぜCodex CLIでGraphQL開発なのか

GraphQL APIの開発には、REST APIとは異なるスキルセットが必要です。Codex CLIを活用することで、以下のメリットが得られます。

従来のアプローチCodex CLI活用
スキーマを手動で定義ドメインモデルから自動生成
リゾルバを1つずつ実装テンプレートから一括生成
N+1問題を事後発見DataLoaderを最初から組み込み
型安全性の確認が手動TypeScript型を自動同期

プロジェクト初期化

# Codex CLIでGraphQLプロジェクトを生成
codex "TypeScript + Apollo Server v4でGraphQL APIプロジェクトを作成してください。

要件:
- Node.js 20 + TypeScript 5.x
- Apollo Server v4
- Prisma ORM(PostgreSQL)
- DataLoader(N+1対策)
- JWT認証
- Zod(入力バリデーション)
- Vitest(テスト)

ディレクトリ構成:
src/
  schema/     # GraphQLスキーマ(.graphql)
  resolvers/  # リゾルバ
  models/     # Prismaモデル
  loaders/    # DataLoader
  middleware/ # 認証・認可
  utils/      # ユーティリティ"

スキーマ設計の自動化

ドメインモデルからスキーマを生成

Codex CLIにビジネス要件を伝えることで、最適なGraphQLスキーマを自動設計できます。

codex "以下のドメインモデルからGraphQLスキーマを設計してください。

【SES案件管理システム】
- プロジェクト: 名前、期間、予算、ステータス、スキル要件
- エンジニア: 名前、スキル、経験年数、単価、稼働状況
- 契約: プロジェクトとエンジニアの紐付け、期間、単価
- スキル: 名前、カテゴリ、レベル

要件:
- ページネーション(Cursor-based)
- フィルタリング・ソート
- リレーション解決
- Mutation(CRUD)
- Subscription(ステータス変更通知)"

Codex CLIが生成するスキーマ例:

# schema/project.graphql
type Project {
  id: ID!
  name: String!
  description: String
  startDate: DateTime!
  endDate: DateTime
  budget: Float!
  status: ProjectStatus!
  requiredSkills: [ProjectSkill!]!
  contracts: [Contract!]!
  createdAt: DateTime!
  updatedAt: DateTime!
}

enum ProjectStatus {
  DRAFT
  RECRUITING
  IN_PROGRESS
  COMPLETED
  CANCELLED
}

type ProjectSkill {
  skill: Skill!
  requiredLevel: SkillLevel!
  priority: Priority!
}

# Cursor-based Pagination
type ProjectConnection {
  edges: [ProjectEdge!]!
  pageInfo: PageInfo!
  totalCount: Int!
}

type ProjectEdge {
  node: Project!
  cursor: String!
}

# Input Types
input ProjectFilterInput {
  status: [ProjectStatus!]
  budgetMin: Float
  budgetMax: Float
  requiredSkills: [ID!]
  searchQuery: String
}

input ProjectSortInput {
  field: ProjectSortField!
  direction: SortDirection!
}

enum ProjectSortField {
  CREATED_AT
  BUDGET
  START_DATE
  NAME
}

# Queries
type Query {
  project(id: ID!): Project
  projects(
    filter: ProjectFilterInput
    sort: ProjectSortInput
    first: Int
    after: String
  ): ProjectConnection!
}

# Mutations
type Mutation {
  createProject(input: CreateProjectInput!): Project!
  updateProject(id: ID!, input: UpdateProjectInput!): Project!
  deleteProject(id: ID!): Boolean!
  assignEngineer(projectId: ID!, engineerId: ID!, input: AssignEngineerInput!): Contract!
}

# Subscriptions
type Subscription {
  projectStatusChanged(projectId: ID): Project!
}

スキーマのバリデーションとベストプラクティス

codex "以下のGraphQLスキーマをレビューして、
ベストプラクティスに基づく改善提案をしてください。

$(cat src/schema/*.graphql)

チェックポイント:
- Nullable/Non-null の適切な使い分け
- ページネーションパターン
- エラーハンドリング
- 命名規則の一貫性
- セキュリティ(過剰なネスト防止)"

リゾルバの自動生成

基本リゾルバの生成

codex "以下のGraphQLスキーマに対応するリゾルバを
TypeScriptで実装してください。

$(cat src/schema/project.graphql)

要件:
- Prisma ORMを使用
- DataLoaderでN+1対策
- 入力バリデーション(Zod)
- エラーハンドリング(GraphQLError)
- ロギング(構造化ログ)"

Codex CLIが生成するリゾルバ例:

// src/resolvers/project.resolver.ts
import { GraphQLError } from 'graphql';
import { z } from 'zod';
import type { Context } from '../context';
import { encodeCursor, decodeCursor } from '../utils/pagination';

const CreateProjectSchema = z.object({
  name: z.string().min(1).max(200),
  description: z.string().max(2000).optional(),
  startDate: z.string().datetime(),
  endDate: z.string().datetime().optional(),
  budget: z.number().positive(),
  requiredSkills: z.array(z.object({
    skillId: z.string().uuid(),
    requiredLevel: z.enum(['BEGINNER', 'INTERMEDIATE', 'ADVANCED', 'EXPERT']),
    priority: z.enum(['MUST', 'PREFERRED', 'NICE_TO_HAVE']),
  })).optional(),
});

export const projectResolvers = {
  Query: {
    project: async (_: unknown, { id }: { id: string }, ctx: Context) => {
      const project = await ctx.prisma.project.findUnique({
        where: { id },
      });
      if (!project) {
        throw new GraphQLError('Project not found', {
          extensions: { code: 'NOT_FOUND' },
        });
      }
      return project;
    },

    projects: async (
      _: unknown,
      { filter, sort, first = 20, after }: {
        filter?: ProjectFilterInput;
        sort?: ProjectSortInput;
        first?: number;
        after?: string;
      },
      ctx: Context
    ) => {
      const take = Math.min(first, 100); // 最大100件
      const cursor = after ? decodeCursor(after) : undefined;

      const where = buildProjectWhere(filter);
      const orderBy = buildProjectOrderBy(sort);

      const [items, totalCount] = await Promise.all([
        ctx.prisma.project.findMany({
          where,
          orderBy,
          take: take + 1, // 次ページ判定用に+1
          ...(cursor ? { cursor: { id: cursor }, skip: 1 } : {}),
        }),
        ctx.prisma.project.count({ where }),
      ]);

      const hasNextPage = items.length > take;
      const edges = items.slice(0, take).map(node => ({
        node,
        cursor: encodeCursor(node.id),
      }));

      return {
        edges,
        pageInfo: {
          hasNextPage,
          hasPreviousPage: !!after,
          startCursor: edges[0]?.cursor,
          endCursor: edges[edges.length - 1]?.cursor,
        },
        totalCount,
      };
    },
  },

  Project: {
    requiredSkills: (parent: { id: string }, _: unknown, ctx: Context) => {
      return ctx.loaders.projectSkills.load(parent.id);
    },
    contracts: (parent: { id: string }, _: unknown, ctx: Context) => {
      return ctx.loaders.projectContracts.load(parent.id);
    },
  },

  Mutation: {
    createProject: async (_: unknown, { input }: { input: unknown }, ctx: Context) => {
      ctx.requireAuth(); // 認証チェック
      const validated = CreateProjectSchema.parse(input);

      return ctx.prisma.project.create({
        data: {
          ...validated,
          requiredSkills: validated.requiredSkills ? {
            create: validated.requiredSkills.map(s => ({
              skillId: s.skillId,
              requiredLevel: s.requiredLevel,
              priority: s.priority,
            })),
          } : undefined,
        },
      });
    },
  },
};

OpenAI Codex CLIによるGraphQL開発ワークフロー

DataLoaderの自動実装

codex "以下のPrismaスキーマに対応するDataLoaderを実装してください。
バッチ処理とキャッシュの設定も含めてください。

$(cat prisma/schema.prisma)"

Codex CLIが生成するDataLoader:

// src/loaders/index.ts
import DataLoader from 'dataloader';
import type { PrismaClient } from '@prisma/client';

export function createLoaders(prisma: PrismaClient) {
  return {
    // プロジェクトのスキル要件をバッチ取得
    projectSkills: new DataLoader(async (projectIds: readonly string[]) => {
      const skills = await prisma.projectSkill.findMany({
        where: { projectId: { in: [...projectIds] } },
        include: { skill: true },
      });
      const grouped = new Map<string, typeof skills>();
      for (const skill of skills) {
        const existing = grouped.get(skill.projectId) || [];
        existing.push(skill);
        grouped.set(skill.projectId, existing);
      }
      return projectIds.map(id => grouped.get(id) || []);
    }),

    // プロジェクトの契約をバッチ取得
    projectContracts: new DataLoader(async (projectIds: readonly string[]) => {
      const contracts = await prisma.contract.findMany({
        where: { projectId: { in: [...projectIds] } },
        include: { engineer: true },
      });
      const grouped = new Map<string, typeof contracts>();
      for (const contract of contracts) {
        const existing = grouped.get(contract.projectId) || [];
        existing.push(contract);
        grouped.set(contract.projectId, existing);
      }
      return projectIds.map(id => grouped.get(id) || []);
    }),

    // エンジニアをバッチ取得(ID→エンジニア)
    engineers: new DataLoader(async (ids: readonly string[]) => {
      const engineers = await prisma.engineer.findMany({
        where: { id: { in: [...ids] } },
      });
      const map = new Map(engineers.map(e => [e.id, e]));
      return ids.map(id => map.get(id) || null);
    }),
  };
}

認証・認可の実装

JWT認証ミドルウェア

codex "Apollo Server v4でJWT認証とRBAC認可を実装してください。

要件:
- JWT(RS256)でアクセストークン検証
- ロール: ADMIN, MANAGER, ENGINEER, VIEWER
- フィールドレベルの認可(@auth ディレクティブ)
- レート制限(ロールごとに異なる制限)"
// src/middleware/auth.ts
import { GraphQLError } from 'graphql';
import jwt from 'jsonwebtoken';
import type { IncomingMessage } from 'http';

interface JWTPayload {
  sub: string;
  role: Role;
  permissions: string[];
  iat: number;
  exp: number;
}

type Role = 'ADMIN' | 'MANAGER' | 'ENGINEER' | 'VIEWER';

const ROLE_HIERARCHY: Record<Role, number> = {
  VIEWER: 0,
  ENGINEER: 1,
  MANAGER: 2,
  ADMIN: 3,
};

export async function getUser(req: IncomingMessage): Promise<JWTPayload | null> {
  const token = req.headers.authorization?.replace('Bearer ', '');
  if (!token) return null;

  try {
    const publicKey = process.env.JWT_PUBLIC_KEY!;
    return jwt.verify(token, publicKey, {
      algorithms: ['RS256'],
    }) as JWTPayload;
  } catch {
    return null;
  }
}

export function requireRole(minRole: Role) {
  return (user: JWTPayload | null) => {
    if (!user) {
      throw new GraphQLError('Authentication required', {
        extensions: { code: 'UNAUTHENTICATED' },
      });
    }
    if (ROLE_HIERARCHY[user.role] < ROLE_HIERARCHY[minRole]) {
      throw new GraphQLError('Insufficient permissions', {
        extensions: { code: 'FORBIDDEN' },
      });
    }
  };
}

エラーハンドリングとバリデーション

統一エラーフォーマット

codex "GraphQL APIの統一エラーハンドリングを実装してください。

要件:
- バリデーションエラー: フィールドごとのエラーメッセージ
- ビジネスロジックエラー: カスタムエラーコード
- 内部エラー: ログに詳細、クライアントにはマスク
- Zodバリデーションとの統合"
// src/utils/errors.ts
import { GraphQLError, GraphQLFormattedError } from 'graphql';
import { ZodError } from 'zod';

export class BusinessError extends GraphQLError {
  constructor(message: string, code: string, details?: Record<string, unknown>) {
    super(message, {
      extensions: {
        code,
        ...details,
      },
    });
  }
}

export function formatError(error: GraphQLFormattedError): GraphQLFormattedError {
  // Zodバリデーションエラーの整形
  if (error.extensions?.originalError instanceof ZodError) {
    const zodError = error.extensions.originalError as ZodError;
    return {
      message: 'Validation failed',
      extensions: {
        code: 'VALIDATION_ERROR',
        fields: zodError.errors.map(e => ({
          path: e.path.join('.'),
          message: e.message,
        })),
      },
    };
  }

  // 内部エラーのマスク
  if (!error.extensions?.code || error.extensions.code === 'INTERNAL_SERVER_ERROR') {
    console.error('Internal error:', error);
    return {
      message: 'An unexpected error occurred',
      extensions: { code: 'INTERNAL_SERVER_ERROR' },
    };
  }

  return error;
}

テスト自動生成

GraphQL APIのテスト

codex "以下のリゾルバに対するテストを Vitest で作成してください。

$(cat src/resolvers/project.resolver.ts)

テスト項目:
- 正常系: CRUD操作
- ページネーション: cursor-based
- フィルタリング: 各条件
- 認証: 未認証・権限不足
- バリデーション: 不正入力
- N+1: DataLoaderが正しく動作"
// src/resolvers/__tests__/project.test.ts
import { describe, it, expect, beforeEach } from 'vitest';
import { createTestContext } from '../../test/helpers';

describe('Project Resolvers', () => {
  let ctx: TestContext;

  beforeEach(async () => {
    ctx = await createTestContext();
  });

  describe('Query.projects', () => {
    it('ページネーションが正しく動作する', async () => {
      // 10件のプロジェクトを作成
      await ctx.createProjects(10);

      const result = await ctx.query(`
        query {
          projects(first: 3) {
            edges { node { id name } cursor }
            pageInfo { hasNextPage endCursor }
            totalCount
          }
        }
      `);

      expect(result.data.projects.edges).toHaveLength(3);
      expect(result.data.projects.pageInfo.hasNextPage).toBe(true);
      expect(result.data.projects.totalCount).toBe(10);

      // 次ページ
      const page2 = await ctx.query(`
        query($after: String!) {
          projects(first: 3, after: $after) {
            edges { node { id } }
            pageInfo { hasNextPage }
          }
        }
      `, { after: result.data.projects.pageInfo.endCursor });

      expect(page2.data.projects.edges).toHaveLength(3);
    });

    it('フィルタリングが正しく動作する', async () => {
      await ctx.createProject({ status: 'RECRUITING', budget: 5000000 });
      await ctx.createProject({ status: 'COMPLETED', budget: 3000000 });

      const result = await ctx.query(`
        query {
          projects(filter: { status: [RECRUITING], budgetMin: 4000000 }) {
            totalCount
          }
        }
      `);

      expect(result.data.projects.totalCount).toBe(1);
    });
  });

  describe('Mutation.createProject', () => {
    it('未認証の場合エラーになる', async () => {
      const result = await ctx.queryAsAnonymous(`
        mutation {
          createProject(input: { name: "Test", startDate: "2026-01-01", budget: 1000000 }) {
            id
          }
        }
      `);

      expect(result.errors[0].extensions.code).toBe('UNAUTHENTICATED');
    });
  });
});

REST APIからの移行

段階的移行戦略

codex "以下のREST APIをGraphQLに段階的に移行する計画を立ててください。

現行REST API:
- GET /api/projects (一覧)
- GET /api/projects/:id (詳細)
- POST /api/projects (作成)
- PUT /api/projects/:id (更新)
- GET /api/engineers (一覧)
- GET /api/engineers/:id/skills (スキル一覧)
- POST /api/contracts (契約作成)

制約:
- フロントエンドは段階的に移行(REST/GraphQL並行運用期間あり)
- ダウンタイムなし
- 既存のクライアントアプリに影響を与えない"

SES現場でのGraphQL案件

需要と単価

スキル月単価目安主な案件タイプ
GraphQL API設計・実装65-85万円BFF開発、マイクロサービス間通信
Apollo Server/Client70-85万円フルスタック開発、SPA連携
GraphQL + TypeScript70-90万円型安全なAPI開発
REST→GraphQL移行75-90万円レガシーモダナイゼーション

案件獲得のポイント

GraphQL案件では以下のスキルセットが評価されます。

  • スキーマ設計力: ドメインモデルを適切にGraphQLスキーマに落とし込める
  • パフォーマンス: DataLoader、クエリ複雑度制限、キャッシング戦略
  • セキュリティ: 認証/認可、レート制限、Introspection制御
  • テスト: E2Eテスト、スキーマ変更の後方互換性チェック
  • フロントエンド連携: Apollo Client、Relay、urqlとの統合経験

Codex CLIを活用することで、これらのスキルを効率的に習得し、実務で即戦力として活躍できます。

まとめ

OpenAI Codex CLIを活用したGraphQL API開発は、以下のワークフローで効率的に進められます。

  1. スキーマ設計: ドメインモデルからGraphQLスキーマを自動生成
  2. リゾルバ実装: DataLoader込みのリゾルバを一括生成
  3. 認証・認可: JWT + RBACの実装をAI支援で効率化
  4. テスト: テストケースの自動生成と実行
  5. 移行: REST APIからの段階的な移行戦略を策定

GraphQLスキルはSES市場で需要が高まっており、Codex CLIを活用した効率的な開発経験は大きな差別化要因になります。

関連記事

SES案件をお探しですか?

SES記事をもっと読む →
🏗️

SES BASE 編集長

SES業界歴10年以上のメンバーが在籍する編集チーム。SES企業での営業・エンジニア経験、フリーランス独立経験を持つメンバーが、業界のリアルな情報をお届けします。

📊 業界データに基づく記事制作 🔍 IPA・経済産業省データ参照 💼 SES実務経験者が執筆・監修