GraphQL × TypeScript × Zod:Code Generator を活用した型安全な API 開発とスキーマ管理

  • typescript
    typescript
  • apollo
    apollo
  • graphql
    graphql
  • github
    github
  • swagger
    swagger
2024/02/12に公開

はじめに

GraphQL を活用した開発において、スキーマと型の管理は非常に重要です。特に、バックエンドとフロントエンドの型を統一し、API の変更を適切に管理することは、開発の効率化とバグの削減につながります。本記事では、GraphQL Code Generator を中心に TypeScript, Zod, GraphQL SDL などの技術を組み合わせ、型安全な API 開発のためのベストプラクティスを紹介します。スキーマ駆動開発のフローを確立し、スキーマ管理やレスポンスバリデーション、API ドキュメントの自動生成、API テストの自動化までを一貫して解説します。

GraphQL を導入したものの、「スキーマ変更の管理が難しい」「フロントエンドで型の不整合が発生する」「レスポンスのバリデーションをどうするべきかわからない」といった課題を感じている方に向けて、具体的な解決策を提供します。

GraphQL Code Generator の導入と型定義の自動生成

GraphQL Code Generator とは?

GraphQL Code Generator は、GraphQL スキーマから TypeScript 型やコードを自動生成するツールです。これにより、フロントエンドとバックエンドでの型の不整合を防ぎ、開発効率を向上できます。

https://the-guild.dev/graphql/codegen

メリット

まず、GraphQL Code Generator を利用する主な目的は、「スキーマから自動生成される型定義を活用し、フロントエンドやバックエンドの実装を効率良く進める」 ことにあります。

  • スキーマを変更すれば自動で型定義が更新される
  • TypeScript と連携して、型安全性を高められる
  • フロントエンド・バックエンド両者の実装が常にスキーマと一致する状態になる

特にチーム開発や大規模プロジェクトでは、型定義の自動生成を取り入れることで、作業効率・バグの減少ともに大きくメリットを得られます。

インストールと基本設定

以下のコマンドで @graphql-codegen/cli をインストールします。

npm install --save-dev @graphql-codegen/cli

次に、必要なプラグインを追加します。TypeScript の型定義を生成する場合は、以下のプラグインをインストールします。

npm install --save-dev @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typescript-react-apollo

まず、GraphQL のスキーマを schema.graphql に定義します。

schema.graphql
type Author {
  id: Int!
  firstName: String!
  lastName: String!
  posts(findTitle: String): [Post]
}
 
type Post {
  id: Int!
  title: String!
  author: Author
}
 
type Query {
  posts: [Post]
}

ポイント

  • これが サーバーのスキーマ定義 であり、この情報をもとに型定義が生成されます。

仮にGraphQL Code Generator を使っていなかった場合に、クライアントサイドでGraphQLにアクセスするとしたら下記のような書き方になります。

import { request, gql } from 'graphql-request'
import { useQuery } from '@tanstack/react-query'
 
interface PostsQuery {
  posts: {
    id: string
    title: string
    author?: {
      id: string
      firstName: string
      lastName: string
    }
  }[]
}
 
const postsQueryDocument = gql`
  query Posts {
    posts {
      id
      title
      author {
        id
        firstName
        lastName
      }
    }
  }
`
 
const Posts = () => {
  const { data } = useQuery<PostsQuery>('posts', async () => {
    const { posts } = await request(endpoint, postsQueryDocument)
    return posts
  })
 
}
interface PostsQuery {
  posts: {
    id: string
    title: string
    author?: {
      id: string
      firstName: string
      lastName: string
    }
  }[]
}

このようにuseQueryのレスポンスの型を事前に用意した上でデータ取得を行う必要があります。

この型定義を省略できるのがGraphQL Code Generatorの役割となります。

設定ファイル codegen.ts を作成し、スキーマと対象となる GraphQL の操作を指定します。

import type { CodegenConfig } from '@graphql-codegen/cli'
 
const config: CodegenConfig = {
   schema: 'https://localhost:4000/graphql',
   documents: ['src/**/*.tsx'],
   generates: {
      './src/gql/': {
        preset: 'client',
      }
   }
}
export default config

次に、以下のコマンドを実行すると、型定義が自動生成されます。

npx graphql-codegen

すると下記のような形でデータを取得することが可能となります。

import { request } from 'graphql-request'
import { useQuery } from '@tanstack/react-query'
import { graphql } from './gql/gql'
 
const postsQueryDocument = graphql(/* GraphQL */ `
  query Posts {
    posts {
      id
      title
      author {
        id
        firstName
        lastName
      }
    }
  }
`)
 
const Posts = () => {
  const { data } = useQuery<PostsQuery>('posts', async () => {
    const { posts } = await request(endpoint, postsQueryDocument)
    return posts
  })
}

このようにレスポンスの型を作らずともスキーマ定義から自動生成された型を使ってデータ取得が可能となります。

GraphQLのスキーマ定義とレスポンス用の(TypeScriptの)型定義をそれぞれ作っていた場合だと、定義の変更の際に両方修正する必要があります。

また状況を把握していない開発者が更新した場合に2つの定義に齟齬が発生する可能性もありデータ取得が失敗する恐れもあります。

そのような観点からも一元的に管理するのが望ましいと考えられます。

スキーマ駆動開発(GraphQL SDL ベースでの開発フロー確立)

スキーマ駆動開発(SDD)とは?

スキーマ駆動開発(SDD: Schema-Driven Development)とは、API の仕様を GraphQL のスキーマ定義(SDL: Schema Definition Language)を基盤に設計・開発を進める手法 です。

従来の REST API では、API 仕様書を別途作成し、フロントエンドとバックエンド間で共有する必要がありました。しかし、GraphQL ではスキーマがそのまま API の仕様となるため、開発の進行がスムーズになります。

メリット

  • 仕様の明確化
    スキーマがそのまま API 仕様となるため、フロントエンド / バックエンド間で仕様の認識相違が起きづらくなります。
  • 自動ドキュメント化
    GraphQL Playground や Apollo Studio などを使えば、スキーマ定義から自動的にドキュメントを生成でき、API の利用者が理解しやすくなります。
  • 変更点の検知が容易
    スキーマに変更がある場合、実装との整合性チェックも自動で発生しやすくなるため、変更の影響範囲が明確になります。

このステップを踏むことで、チーム全体の作業効率も上がり、スムーズなコミュニケーションとプロジェクト運営を実現できます。

開発フロー

スキーマ定義作成

type Author {
  id: Int!
  firstName: String!
  lastName: String!
}
 
type Post {
  id: Int!
  title: String!
  author: Author
}
 
type Query {
  posts: [Post] # 単一の投稿を取得する
  post(id: ID!): Post # 単一の投稿を取得する
}

このスキーマが API のベースとして機能し、フロントエンド・バックエンド間での共通認識となります。

4. バックエンド実装

スキーマから生成した TypeScript 型を活用して、バックエンドのリゾルバを実装します。

メリット

  • スキーマが API の仕様書になるため、開発の方向性がブレない
  • 実装がスキーマに沿っているかを保証できる

リゾルバの定義

resolvers.ts
// ダミーデータ
const posts = [
  { id: 1, title: "GraphQL Introduction", authorId: 1 },
  { id: 2, title: "Advanced GraphQL", authorId: 1 },
  { id: 3, title: "GraphQL with Apollo", authorId: 2 },
];

export const resolvers = {
  Query: {
    // すべての投稿を取得
    posts: () => posts,

    // IDで特定の投稿を取得
    post: (_parent: any, args: { id: number }) =>
      posts.find((post) => post.id === args.id),
  },
};

GraphQLスキーマ定義を gql(GraphQL タグ)としてパースします。

type.ts
import { readFileSync } from "fs";
import { gql } from "graphql-tag";
import path from "path";

const schemaPath = path.resolve(process.cwd(), "../packages/graphql/schema/post.graphql");

const typeDefs = gql(readFileSync(schemaPath, "utf-8"));

export { typeDefs };

最後に、GraphQL サーバー (server) を Express に組み込んで起動します。

index.ts
import express from "express";
import cors from "cors";
import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@apollo/server/express4';
import { resolvers } from './graphql/resolvers';
import { typeDefs } from "./graphql/type";

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

const app = express();
const PORT = process.env.PORT || 4000;

const startServer = async () => {
  await server.start();

  app.use(
    '/graphql',
    cors(),
    express.json(),
    expressMiddleware(server)
  );

  app.listen(PORT, () => {
    console.log('GraphQL server is running at http://localhost:4000/graphql');
  });
};

startServer();

スキーマを基に TypeScript 型を自動生成

GraphQL のスキーマを基に GraphQL Code Generatorを使ってTypeScript の型を自動生成 することで、型安全な開発を実現します。

メリット

  • API 変更時に型エラーが発生するため、修正漏れを防げる
  • クライアント側の開発がより安全になる

GraphQL Code Generator (@graphql-codegen/cli) の設定ファイル を作成し、
GraphQL のスキーマとクエリから 型付きの TypeScript コードを自動生成するための設定を行います。

import { CodegenConfig } from '@graphql-codegen/cli';
 
const config: CodegenConfig = {
  schema: '../packages/graphql/schema/**/*.graphql',
  generates: {
    "generated/graphql.ts": {
      plugins: [
        'typescript',
        'typescript-operations',
      ],
    },
  } 
};
export default config;

TypeScriptの型が生成されます。

export type Maybe<T> = T | null;
export type InputMaybe<T> = Maybe<T>;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
export type MakeEmpty<T extends { [key: string]: unknown }, K extends keyof T> = { [_ in K]?: never };
export type Incremental<T> = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never };
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
  ID: { input: string; output: string; }
  String: { input: string; output: string; }
  Boolean: { input: boolean; output: boolean; }
  Int: { input: number; output: number; }
  Float: { input: number; output: number; }
};

export type Author = {
  __typename?: 'Author';
  firstName: Scalars['String']['output'];
  id: Scalars['Int']['output'];
  lastName: Scalars['String']['output'];
  posts?: Maybe<Array<Maybe<Post>>>;
};


export type AuthorPostsArgs = {
  findTitle?: InputMaybe<Scalars['String']['input']>;
};

export type Post = {
  __typename?: 'Post';
  author?: Maybe<Author>;
  id: Scalars['Int']['output'];
  title: Scalars['String']['output'];
};

export type Query = {
  __typename?: 'Query';
  post?: Maybe<Post>;
  posts?: Maybe<Array<Maybe<Post>>>;
};


export type QueryPostArgs = {
  id: Scalars['ID']['input'];
};

フロントエンド実装

スキーマから生成した TypeScript 型を活用して、フロントエンド側で GraphQL クエリを記述します。

メリット

  • API 仕様と実装のズレを防ぐ
  • スキーマ変更が型に反映されるため、型エラーで検知しやすい

まずはGraphQLクエリを作成します。

query GetPosts {
  posts {
    id
    title
    author {
      firstName
      lastName
    }
  }
}

Code Generatorのconfigを設定変更します。

apps/frontend/config/config.ts
import { CodegenConfig } from '@graphql-codegen/cli';
 
const config: CodegenConfig = {
  schema: '../packages/graphql/schema/**/*.graphql',
  documents: '../packages/graphql/operations/**/*.graphql',
  generates: {
    "generated/graphql.ts": {
      plugins: [
        'typescript',
        'typescript-operations',
        'typescript-graphql-request',
      ],
    },
  } 
};
export default config;

すると新たにクエリに関する定義と戻り値が用意され、フロントエンドで使うことができます。

export type GetPostsQuery = { 
  __typename?: 'Query', 
  posts?: Array<{
     __typename?: 'Post', 
     id: number, 
     title: string, 
     author?: { 
      __typename?: 'Author', 
      firstName: string, 
      lastName: string 
    } | null 
  } | null> | null 
};

export const GetPostsDocument = gql`
    query GetPosts {
  posts {
    id
    title
    author {
      firstName
      lastName
    }
  }
}
    `;

export type SdkFunctionWrapper = <T>(action: (requestHeaders?:Record<string, string>) => Promise<T>, operationName: string, operationType?: string, variables?: any) => Promise<T>;


const defaultWrapper: SdkFunctionWrapper = (action, _operationName, _operationType, _variables) => action();

export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper = defaultWrapper) {
  return {
    GetPosts(variables?: GetPostsQueryVariables, requestHeaders?: GraphQLClientRequestHeaders): Promise<GetPostsQuery> {
      return withWrapper((wrappedRequestHeaders) => client.request<GetPostsQuery>(GetPostsDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'GetPosts', 'query', variables);
    }
  };
}
export type Sdk = ReturnType<typeof getSdk>;
service.ts
import { GraphQLClient } from "graphql-request";
import { getSdk } from "../generated/graphql"; 

const client = new GraphQLClient("http://localhost:4000/graphql");
const sdk = getSdk(client);

export async function getPost() {
  const response = await sdk.GetPosts();
  return response.posts
}
import { useEffect, useState } from "react";
import { GetPostsQuery } from "../generated/graphql";
import { getPosts } from "./service";

function App() {
  const [posts, setPosts] = useState<GetPostsQuery["posts"] | null>(null);

  useEffect(() => {
    getPosts()
      .then((data) => {
        setPosts(data);
      })
      .catch((error) => {
        console.error(error);
      });
  }, []);

  return (
    <div>
      <h1>React + Express</h1>
      {posts === null || posts === undefined ? (
        <p>Loading...</p>
      ) : posts.length === 0 ? (
        <p>No posts found.</p>
      ) : (
        <ul>
          {posts.map((post) => (
            <li key={post?.id}>
              <h2>{post?.title}</h2>
              <p>Author: {post?.author?.firstName} {post?.author?.lastName}</p>
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

export default App;

GraphQL のスキーマ管理(変更履歴の追跡・バージョン管理)

GraphQL のスキーマは、フロントエンドとバックエンドの通信を規定する契約のようなものです。そのため、スキーマに変更が加わると、両者の整合性が崩れる可能性があります。

特に、破壊的変更(Breaking Changes) があると、クライアント側のコードが動かなくなるリスクがあるため、スキーマのバージョン管理や変更履歴の追跡が重要です。

Git を活用したスキーマのバージョン管理

スキーマファイルを Git リポジトリで管理 し、変更履歴を追跡することで、いつ・どのような変更が行われたかを把握できます。

またGitHub では、GitHub Actions を活用して、schema.graphql に変更があった場合に PR に自動でラベルを付与 することができます。

1. .github/labeler.yml を作成(スキーマ変更を検知する設定)

.github/labeler.yml
schema-change:
  - "apps/packages/graphql/schema/**/*.graphql"
  • スキーマファイルの変更のみを検知します。

2. GitHub Actions ワークフローを作成(PR にラベルを付与) .github/workflows/label-schema-changes.yml

.github/labeler.yml
name: "Add label for GraphQL schema changes"

on:
  pull_request:

permissions:
  pull-requests: write
  contents: read

jobs:
  label:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Apply labels
        uses: actions/labeler@v4
        with:
          repo-token: "${{ secrets.GITHUB_TOKEN }}"
  • apps/packages/graphql/schema/**/*.graphql に変更があった場合、PR に schema-change というラベルが自動で付きます。

Image from Gyazo

PRを作成すると

  • ①Labelを付与するGitHub Actions ワークフローが実行され
  • ②Labelsにschema-changeがセットされます。

labelで知らせるだけでなくGitHub Actions ワークフローのslack連携を使用すると通知を行うなども可能です。

スキーマのマイグレーション(Schema Migration)

GraphQL スキーマを直接変更するのではなく、段階的な移行 を行うことで、クライアントへの影響を最小限に抑えることができます。

安全なスキーマ変更の流れ

  1. 非破壊的変更(Backward Compatible)を優先
    • フィールドの追加
    • デフォルト値の変更
  2. 破壊的変更が必要な場合は Deprecation(非推奨化)
    • @deprecated ディレクティブを使って、古いフィールドを非推奨にする
    • クライアント側で対応が完了した後に削除する

例)

type Post {
  id: Int!
  title: String!
  description: String @deprecated(reason: "Use 'excerpt' instead.")
  author: Author
}

このように @deprecated をつけることで、クライアントに「このフィールドは将来的に削除される」ということを通知できます。

GraphQL Playgroundでクエリを作成する際にも非推奨であることが表示され開発者に知らせることが可能です。

Image from Gyazo

バックエンドのレスポンスバリデーション(Zod などを活用)

バックエンドのレスポンスバリデーションとは、API からクライアントへ返されるデータが期待した形式や構造になっているかを検証するプロセスです。適切なバリデーションを行うことで、フロントエンドでのエラー防止やセキュリティ強化につながります。

なぜレスポンスバリデーションが必要なのか

  1. データ整合性の確保
    バックエンドが予期せぬデータを返す可能性があるため、フロントエンドが誤動作しないようにするために必要です。

  2. セキュリティリスクの軽減
    不正なデータがレスポンスに含まれてしまうと、XSS やデータ改ざんといったセキュリティリスクが発生します。

  3. バグの早期発見
    API の変更が原因でレスポンスの構造が変わった場合、バリデーションによって問題を即座に検知できます。

レスポンスバリデーションの実装方法

  1. スキーマバリデーションを導入する
    JSON Schema などを使用して、レスポンスの構造を定義し、それに基づいたバリデーションを行うのが一般的です。

例: Zod を使用したバリデーション(Node.js)

import { z } from 'zod';

const userSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email(),
});

const validateResponse = (data: any) => {
  const result = userSchema.safeParse(data);
  if (!result.success) {
    console.error("Invalid response data:", result.error.format());
    throw new Error("Response validation failed");
  }
  return result.data;
};
  1. バックエンド側でバリデーションを実装する
    バックエンドのレスポンスを生成する際に、データの型や値のチェックを行うことで、不正なデータを返さないようにします。

Express でのレスポンスバリデーション例

import express from 'express';
import { z } from 'zod';

const app = express();

const responseSchema = z.object({
  success: z.boolean(),
  data: z.object({
    id: z.number(),
    name: z.string(),
  }),
});

app.get('/user', (req, res) => {
  const responseData = {
    success: true,
    data: { id: 1, name: "Taro" },
  };

  const validationResult = responseSchema.safeParse(responseData);
  if (!validationResult.success) {
    return res.status(500).json({ error: "Invalid response format" });
  }

  res.json(responseData);
});

app.listen(3000, () => console.log("Server running on port 3000"));

フェッチ時の型チェック強化(フロント側での不整合防止)

フロントエンド開発では、APIから取得したデータの型が期待通りでない場合に、アプリケーションの挙動が不安定になることがあります。本記事では、フェッチ時の型チェックを強化し、フロントエンド側での不整合を防ぐための方法を解説します。

APIレスポンスの型定義

TypeScriptを活用して、APIから返されるデータの型を定義することで、型安全な開発が可能になります。

interface User {
  id: number;
  name: string;
  email: string;
}

async function fetchUser(): Promise<User> {
  const response = await fetch("https://api.example.com/user");
  const data = await response.json();
  return data; // 型チェックを強化するためにバリデーションを追加
}

型アサーションを避ける

型アサーション (as Type) を多用すると、実際には不整合なデータが渡された場合でもコンパイルエラーを回避してしまうため、適切な型チェックが機能しません。

async function fetchUser(): Promise<User> {
  const response = await fetch("https://api.example.com/user");
  const data = await response.json();
  return data as User; // ⚠️ 型アサーションのみだと不正なデータが入りうる
}

Zodを活用した型安全なデータチェック

Zodなどのスキーマバリデーションライブラリを活用すると、APIレスポンスの構造を厳密に検証できます。

import { z } from "zod";

const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email(),
});

type User = z.infer<typeof UserSchema>;

async function fetchUser(): Promise<User> {
  const response = await fetch("https://api.example.com/user");
  const data = await response.json();
  return UserSchema.parse(data); // ここで型チェックを実施
}

型ガードを活用した手動チェック

バリデーションライブラリを使わずに、手動で型チェックを行うことも可能です。

function isUser(data: unknown): data is User {
  return (
    typeof data === "object" && data !== null &&
    "id" in data && typeof (data as any).id === "number" &&
    "name" in data && typeof (data as any).name === "string" &&
    "email" in data && typeof (data as any).email === "string"
  );
}

async function fetchUser(): Promise<User> {
  const response = await fetch("https://api.example.com/user");
  const data = await response.json();
  if (!isUser(data)) {
    throw new Error("Invalid API response");
  }
  return data;
}

GraphQL Code Generator を活用した型安全な開発

import { gql } from "graphql-tag";
import { request } from "graphql-request";
import { UserQuery } from "@generated/graphql";

const GET_USER = gql`
  query getUser($id: ID!) {
    user(id: $id) {
      id
      name
      email
    }
  }
`;

async function fetchUser(id: string): Promise<UserQuery> {
  const response = await request("/graphql", GET_USER, { id });
  return response;
}

このように、GraphQL の型を適用することで、API レスポンスの型を保証できます。

API ドキュメントの自動生成

API の開発において、ドキュメントの整備とテストの自動化は重要な要素です。GraphQL Playground や Swagger を活用した API ドキュメントの自動生成、および Mock サーバーを利用した API テストの自動化について解説します。

GraphQL Playground を活用した API ドキュメント

GraphQL では、スキーマ定義を基に動的に API ドキュメントを生成できます。GraphQL Playground は、API のエンドポイントを視覚的に確認でき、スキーマを即時にドキュメント化する便利なツールです。

  1. Playground を有効化するには以下のように設定します。
import { ApolloServer, gql } from 'apollo-server';

const typeDefs = gql`
  type Query {
    hello: String
  }
`;

const resolvers = {
  Query: {
    hello: () => 'Hello, world!',
  },
};

const server = new ApolloServer({ typeDefs, resolvers });
server.listen().then(({ url }) => {
  console.log(`Server ready at ${url}`);
});
  1. サーバーを起動すると、http://localhost:4000 で Playground にアクセス可能になります。

  2. GraphQL スキーマの変更を即座に反映し、開発者向けの API ドキュメントを自動生成できます。

下記のようにフィールド情報が表示され、サンプルのクエリを作成する際にはコード補完もされます。

Image from Gyazo

Swagger を利用した REST API ドキュメント

REST API のドキュメント自動生成には Swagger(OpenAPI)を活用します。

  1. Express ベースの API に swagger-jsdocswagger-ui-express を追加します。
npm install swagger-jsdoc swagger-ui-express
  1. Swagger 設定を追加します。
import express from 'express';
import swaggerJsdoc from 'swagger-jsdoc';
import swaggerUi from 'swagger-ui-express';

const app = express();

const swaggerOptions = {
  definition: {
    openapi: '3.0.0',
    info: {
      title: 'Sample API',
      version: '1.0.0',
    },
  },
  apis: ['./routes/*.js'],
};

const swaggerDocs = swaggerJsdoc(swaggerOptions);
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocs));

app.listen(3000, () => console.log('Server running on port 3000'));
  1. http://localhost:3000/api-docs にアクセスすると、Swagger UI で API ドキュメントが確認できます。

API テストの自動化(Mock サーバーの活用)

API のテストを効率的に行うために、Mock サーバーを活用します。json-server や GraphQL の @graphql-tools/mock を利用すると、バックエンドの開発前でもフロントエンドの開発・テストを進めることができます。

JSON Server を使った Mock REST API

json-server は、シンプルな JSON ファイルをもとに REST API を提供できる便利なツールです。これにより、バックエンドの実装が完了していなくても、フロントエンドの開発やテストを進めることができます。

  1. json-server をインストールします。

    npm install -g json-server
    
  2. db.json を作成し、ダミーデータを用意します。

    {
      "users": [
        { "id": 1, "name": "Alice" },
        { "id": 2, "name": "Bob" }
      ]
    }
    
  3. json-server を起動します。

    json-server --watch db.json --port 3001
    
  4. http://localhost:3001/users にアクセスすると、Mock API が動作していることを確認できます。

なお、json-server には以下のようなカスタマイズ機能があります。

  • カスタムルートの設定: routes.json を作成し、エンドポイントをカスタマイズできます。

    {
      "/api/users": "/users"
    }
    
  • json-serverExpress のようにカスタマイズするためのものです。通常、json-server は CLI から簡単に使えますが、Express のように拡張することで、リクエストをより柔軟に処理できます。

const jsonServer = require('json-server');
const server = jsonServer.create();  // JSON Server のインスタンスを作成
const router = jsonServer.router('db.json');  // db.json をもとにルーティングを生成
const middlewares = jsonServer.defaults();  // JSON Server のデフォルトミドルウェア(CORS, ロギング, JSON パースなど)を適用

server.use(middlewares);  // ミドルウェア適用
server.use((req, res, next) => {
  console.log('Request received:', req.method, req.url);  // リクエストログを出力
  next();  // 次のミドルウェアへ処理を渡す
});
server.use(router);  // ルーターを適用

server.listen(3001, () => {
  console.log('JSON Server is running on port 3001');
});

ポイント

  • すべてのリクエストをコンソールに出力することで、デバッグしやすくなる。
  • json-server の標準ログに加え、リクエストの詳細な制御が可能。
server.use('/users', (req, res, next) => {
  req.query.limit = 10;  // クエリパラメータを強制的に追加
  next();
});

ポイント

  • /users にアクセスしたときにリクエストのデータを変更することも可能。
router.render = (req, res) => {
  res.json({
    success: true,
    data: res.locals.data
  });
};
  • json-server のデフォルトレスポンスを変更して、カスタムレスポンスを返すことも可能。
  • すべてのレスポンスに success: true を追加できる。

GraphQL Mock サーバーの構築

  1. @graphql-tools/mock を使用して GraphQL Mock サーバーを構築します。
npm install @graphql-tools/mock @graphql-tools/schema graphql-tag
  1. Mock API の設定を追加します。
import { makeExecutableSchema } from '@graphql-tools/schema';
import { addMocksToSchema } from '@graphql-tools/mock';
import { graphql } from 'graphql';

const typeDefs = `
  type User {
    id: ID!
    name: String!
  }
  type Query {
    users: [User]
  }
`;

const schema = makeExecutableSchema({ typeDefs });
const mockedSchema = addMocksToSchema({ schema });

graphql(mockedSchema, '{ users { id name } }').then((result) =>
  console.log(JSON.stringify(result, null, 2))
);
  1. スキーマに基づいた Mock API が即座に生成され、テストが可能になります。

さいごに

GraphQL Code Generator を活用することで、GraphQL のスキーマ管理や型定義の自動生成を効率化し、バックエンドとフロントエンドの型の不整合を防ぐことができます。また、Zod を用いたレスポンスバリデーション や フェッチ時の型チェック強化 によって、安全なデータの受け渡しを実現できます。さらに、API ドキュメントの自動生成や Mock サーバーを活用した API テストの自動化により、開発の効率を向上させることが可能です。

本記事で紹介した手法を活用することで、GraphQL の強みを最大限に活かした型安全な API 開発 が実現できます。ぜひ、プロジェクトに適用し、よりスムーズな開発体験を目指してみてください。

Xでシェア
Facebookでシェア
LinkedInでシェア

記事に関するお問い合わせ📝

記事の内容に関するご質問、ご意見などは、下記よりお気軽にお問い合わせください。
ご質問フォームへ

技術支援などお仕事に関するお問い合わせ📄

技術支援やお仕事のご依頼に関するお問い合わせは、下記よりお気軽にお問い合わせください。
お問い合わせフォームへ

関連する技術ブログ

弊社の技術支援サービス

お問い合わせ

経営と現場をつなぐ“共創型”の技術支援。
成果に直結するチーム・技術・プロセスを共に整えます。

お問い合わせ