T-CREATOR

NestJS と GraphQL を組み合わせた型安全な API 開発

NestJS と GraphQL を組み合わせた型安全な API 開発

現代の Web 開発において、API の型安全性は開発効率と品質向上の鍵となっています。NestJS と GraphQL の組み合わせは、TypeScript の強力な型システムを活用しながら、柔軟で効率的な API 開発を実現する最適なソリューションです。

この記事では、NestJS と GraphQL を組み合わせた型安全な API 開発について、基礎から実践まで詳しく解説いたします。従来の REST API の課題から始まり、GraphQL と NestJS の特徴、そして実際のプロジェクト実装まで段階的にご紹介していきます。

背景

従来の REST API の課題

REST API は長年にわたって Web API の標準として利用されてきましたが、現代の複雑なアプリケーション開発においていくつかの課題が明らかになっています。

まず、オーバーフェッチング問題があります。クライアントが必要とするデータの一部しか使わないにも関わらず、API エンドポイントは固定されたデータ構造を返すため、不要なデータも含めて取得してしまいます。

次に、アンダーフェッチング問題です。単一の画面を構築するために複数の API エンドポイントを呼び出す必要があり、ネットワークリクエストが増加してパフォーマンスに影響を与えます。

さらに、API 仕様の管理が複雑になりがちです。エンドポイントが増加するにつれて、どのエンドポイントがどのようなデータを返すのか把握することが困難になります。

以下の図は、REST API におけるデータ取得の課題を示しています。

mermaidflowchart TD
    Client[クライアント] -->|GET /users| API1[Users API]
    Client -->|GET /posts| API2[Posts API]
    Client -->|GET /comments| API3[Comments API]

    API1 -->|全ユーザー情報| Client
    API2 -->|全投稿情報| Client
    API3 -->|全コメント情報| Client

    Client -.->|実際に使用| UseData[必要な一部データのみ]

    style UseData fill:#f9f,stroke:#333,stroke-width:2px

図で理解できる要点:

  • 複数の API エンドポイントへのリクエストが必要
  • 各 API から不要なデータも含めて取得される
  • 実際に使用されるのは一部のデータのみ

GraphQL の台頭とその特徴

GraphQL は、Facebook によって開発されたクエリ言語およびランタイムです。REST API の課題を解決するために設計されており、以下の特徴があります。

単一エンドポイントによるデータアクセスが可能です。複数のリソースに対するクエリを一度のリクエストで実行できるため、ネットワークのオーバーヘッドを削減できます。

必要なデータのみ取得できる仕組みを提供します。クライアントが明示的に指定したフィールドのみがレスポンスに含まれるため、オーバーフェッチング問題が解決されます。

強力な型システムを持っているため、API スキーマが明確に定義され、クライアントとサーバー間の契約が保証されます。

以下の図は、GraphQL のデータ取得方式を示しています。

mermaidsequenceDiagram
    participant Client as クライアント
    participant GraphQL as GraphQL API
    participant Resolver1 as Users Resolver
    participant Resolver2 as Posts Resolver
    participant DB as データベース

    Client->>GraphQL: 単一クエリリクエスト
    Note over Client,GraphQL: query { user { name } posts { title } }

    GraphQL->>Resolver1: ユーザー名取得
    GraphQL->>Resolver2: 投稿タイトル取得

    Resolver1->>DB: SELECT name FROM users
    Resolver2->>DB: SELECT title FROM posts

    DB-->>Resolver1: ユーザー名
    DB-->>Resolver2: 投稿タイトル

    Resolver1-->>GraphQL: 必要なデータのみ
    Resolver2-->>GraphQL: 必要なデータのみ

    GraphQL-->>Client: 統合されたレスポンス

図で理解できる要点:

  • 単一のリクエストで複数のデータソースから情報を取得
  • 必要なフィールドのみを指定してデータを取得
  • サーバー側で効率的にデータを組み合わせて返却

NestJS が選ばれる理由

NestJS は、Node.js のための効率的でスケーラブルなサーバーサイドアプリケーションを構築するフレームワークです。TypeScript で書かれており、Express(デフォルト)や Fastify をベースとして動作します。

Angular ライクなアーキテクチャを採用しており、デコレーター、依存性注入、モジュールシステムなどの概念を使用します。これにより、大規模なアプリケーションでも保守性の高いコードを書くことができます。

TypeScript ファーストの設計により、開発時点での型チェックが可能で、リファクタリングや IDE サポートが充実しています。

豊富なエコシステムを持ち、GraphQL、WebSocket、マイクロサービス、テストなど、多様な機能を公式でサポートしています。

課題

GraphQL 導入時の型安全性の確保

GraphQL を導入する際の最大の課題の一つが、型安全性の確保です。GraphQL スキーマと TypeScript の型定義が別々に管理されると、以下の問題が発生します。

型の不整合が起こりやすくなります。GraphQL スキーマを変更しても TypeScript の型定義が更新されない場合、ランタイムエラーが発生する可能性があります。

開発効率の低下も問題です。手動で型定義を同期する必要があり、開発者の負担が増加します。

テストの複雑化により、型の整合性を確認するための追加のテストが必要になります。

スキーマと TypeScript の型定義の二重管理問題

従来の GraphQL 実装では、以下のような二重管理の問題があります。

GraphQL スキーマファイル(.graphql)と TypeScript の型定義ファイル(.ts)を別々に管理する必要があり、変更時に両方のファイルを更新する必要があります。

変更の同期タイミングがずれることで、一時的に型の不整合が発生するリスクがあります。

チーム開発において、片方の変更を忘れるリスクが常に存在します。

開発効率とコード品質のバランス

型安全性を重視しすぎると開発速度が低下し、開発速度を重視しすぎると品質に問題が生じるというジレンマがあります。

以下の図は、この課題の構造を示しています。

mermaidflowchart LR
    subgraph Traditional [従来のアプローチ]
        Schema[GraphQL Schema] -.->|手動同期| Types[TypeScript Types]
        Schema -.->|管理負荷| Dev[開発者]
        Types -.->|管理負荷| Dev
    end

    subgraph Issues [発生する問題]
        Sync[同期ずれ]
        Error[型エラー]
        Overhead[管理オーバーヘッド]
    end

    Traditional --> Issues

    style Schema fill:#ffeb3b
    style Types fill:#ffeb3b
    style Issues fill:#f44336,color:#fff

図で理解できる要点:

  • GraphQL スキーマと TypeScript 型定義の二重管理が必要
  • 手動同期による管理負荷とエラーリスク
  • 開発効率と品質のトレードオフが発生

解決策

NestJS + GraphQL による統一的な型定義

NestJS は、GraphQL と TypeScript を統合する優れたソリューションを提供します。**単一の真実の源(Single Source of Truth)**として機能し、型定義の重複を排除します。

TypeScript のクラスとデコレーターを使用して、GraphQL スキーマと TypeScript 型を同時に定義できます。これにより、型の不整合を根本的に防ぐことができます。

リフレクション機能により、TypeScript の型情報から GraphQL スキーマを自動生成し、開発者が手動で管理する必要がありません。

Code First アプローチの採用

NestJS では、Code FirstSchema Firstの二つのアプローチが選択できますが、型安全性を重視する場合は Code First アプローチが推奨されます。

Code First アプローチでは、TypeScript のクラス定義から GraphQL スキーマが自動生成されるため、型の整合性が保証されます。

開発者は TypeScript のコードのみに集中でき、GraphQL スキーマファイルの管理が不要になります。

以下の図は、Code First アプローチの動作を示しています。

mermaidflowchart TB
    subgraph CodeFirst["Code Firstアプローチ"]
        TSClass["TypeScriptクラス定義"];
        Decorators["@Field(), @ObjectType() デコレーター"];

        TSClass --> Decorators;
        Decorators --> AutoGen["自動生成プロセス"];
    end

    subgraph Generated["自動生成される成果物"]
        GraphQLSchema["GraphQL Schema"];
        TypeDefs["型定義"];
    end

    AutoGen --> Generated;

    subgraph Benefits["メリット"]
        SingleSource["単一の定義源"];
        TypeSafe["型安全性"];
        NoSync["同期不要"];
    end

    Generated --> Benefits;

    style TSClass fill:#4caf50
    style AutoGen fill:#2196f3
    style Benefits fill:#ff9800

図で理解できる要点:

  • TypeScript クラス定義が唯一の情報源となる
  • デコレーターにより GraphQL メタデータを追加
  • 自動生成により型安全性を保証

自動生成される型定義の活用

NestJS の GraphQL モジュールは、以下の機能を自動で提供します。

GraphQL スキーマファイルの自動生成により、schema.gql ファイルが自動で作成・更新されます。

TypeScript 型定義の自動生成により、graphql.ts ファイルにすべての型定義が出力されます。

リアルタイム更新により、コードの変更と同時にスキーマと型定義が更新されます。

具体例

プロジェクトセットアップ

まず、新しい NestJS プロジェクトを作成し、GraphQL の設定を行います。

typescript// 必要なパッケージのインストール
// yarn add @nestjs/graphql @nestjs/apollo apollo-server-express graphql

プロジェクトの基本構造を作成します。以下のコマンドで NestJS プロジェクトを初期化できます。

bash# NestJS CLIのインストール
yarn global add @nestjs/cli

# 新しいプロジェクトを作成
nest new graphql-api-project
cd graphql-api-project

# GraphQL関連のパッケージをインストール
yarn add @nestjs/graphql @nestjs/apollo apollo-server-express graphql

次に、メインモジュールで GraphQL を設定します。

typescript// src/app.module.ts
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import {
  ApolloDriver,
  ApolloDriverConfig,
} from '@nestjs/apollo';
import { join } from 'path';

@Module({
  imports: [
    GraphQLModule.forRoot<ApolloDriverConfig>({
      driver: ApolloDriver,
      autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
      sortSchema: true,
    }),
  ],
})
export class AppModule {}

この設定により、TypeScript クラス定義から GraphQL スキーマが自動生成され、src​/​schema.gqlファイルに出力されます。sortSchema: trueオプションにより、生成されるスキーマが読みやすい形式で整理されます。

DTO と Entity の定義

データ転送オブジェクト(DTO)とエンティティを定義します。NestJS では、同一のクラスが GraphQL の型定義と TypeScript の型として機能します。

typescript// src/users/entities/user.entity.ts
import { ObjectType, Field, ID } from '@nestjs/graphql';

@ObjectType()
export class User {
  @Field(() => ID)
  id: string;

  @Field()
  email: string;

  @Field()
  name: string;

  @Field({ nullable: true })
  bio?: string;

  @Field()
  createdAt: Date;
}

@ObjectType()デコレーターにより、このクラスが GraphQL のオブジェクト型として認識されます。@Field()デコレーターで各プロパティが GraphQL フィールドとして定義されます。

入力用の DTO も定義します。

typescript// src/users/dto/create-user.input.ts
import { InputType, Field } from '@nestjs/graphql';
import {
  IsEmail,
  IsNotEmpty,
  MaxLength,
} from 'class-validator';

@InputType()
export class CreateUserInput {
  @Field()
  @IsEmail()
  email: string;

  @Field()
  @IsNotEmpty()
  @MaxLength(50)
  name: string;

  @Field({ nullable: true })
  @MaxLength(200)
  bio?: string;
}

@InputType()デコレーターにより入力型として定義され、class-validatorのデコレーターでバリデーションルールも同時に設定できます。

Resolver の実装

GraphQL のリゾルバーを実装します。リゾルバーは、GraphQL クエリやミューテーションを実際の処理に変換する役割を担います。

typescript// src/users/users.resolver.ts
import {
  Resolver,
  Query,
  Mutation,
  Args,
  ID,
} from '@nestjs/graphql';
import { User } from './entities/user.entity';
import { CreateUserInput } from './dto/create-user.input';
import { UsersService } from './users.service';

リゾルバークラスの本体を実装します。

typescript@Resolver(() => User)
export class UsersResolver {
  constructor(
    private readonly usersService: UsersService
  ) {}

  @Query(() => [User], { name: 'users' })
  findAll(): Promise<User[]> {
    return this.usersService.findAll();
  }

  @Query(() => User, { name: 'user' })
  findOne(
    @Args('id', { type: () => ID }) id: string
  ): Promise<User> {
    return this.usersService.findOne(id);
  }
}

ミューテーション(データ変更操作)も実装します。

typescript@Mutation(() => User)
createUser(@Args('createUserInput') createUserInput: CreateUserInput): Promise<User> {
  return this.usersService.create(createUserInput);
}

@Mutation(() => User)
updateUser(
  @Args('id', { type: () => ID }) id: string,
  @Args('updateUserInput') updateUserInput: UpdateUserInput,
): Promise<User> {
  return this.usersService.update(id, updateUserInput);
}

クエリ・ミューテーションの実装

サービスクラスで実際のビジネスロジックを実装します。

typescript// src/users/users.service.ts
import { Injectable } from '@nestjs/common';
import { User } from './entities/user.entity';
import { CreateUserInput } from './dto/create-user.input';

サービスの実装部分です。実際の開発では、データベースアクセスやビジネスロジックがここに実装されます。

typescript@Injectable()
export class UsersService {
  private users: User[] = [];

  async findAll(): Promise<User[]> {
    return this.users;
  }

  async findOne(id: string): Promise<User> {
    const user = this.users.find((user) => user.id === id);
    if (!user) {
      throw new Error(`User with ID ${id} not found`);
    }
    return user;
  }

  async create(
    createUserInput: CreateUserInput
  ): Promise<User> {
    const user: User = {
      id: Date.now().toString(),
      ...createUserInput,
      createdAt: new Date(),
    };
    this.users.push(user);
    return user;
  }
}

最後に、モジュールを設定してすべてのコンポーネントを統合します。

typescript// src/users/users.module.ts
import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
import { UsersResolver } from './users.resolver';

@Module({
  providers: [UsersResolver, UsersService],
})
export class UsersModule {}

型安全性の検証

実装した機能の型安全性を確認します。NestJS が自動生成する GraphQL スキーマを確認してみましょう。

graphql# 自動生成される schema.gql
type User {
  id: ID!
  email: String!
  name: String!
  bio: String
  createdAt: DateTime!
}

input CreateUserInput {
  email: String!
  name: String!
  bio: String
}

type Query {
  users: [User!]!
  user(id: ID!): User!
}

type Mutation {
  createUser(createUserInput: CreateUserInput!): User!
}

TypeScript の型チェック機能により、コンパイル時にエラーが検出されます。以下は型安全性を検証するテストコードです。

typescript// src/users/users.resolver.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { UsersResolver } from './users.resolver';
import { UsersService } from './users.service';

describe('UsersResolver', () => {
  let resolver: UsersResolver;

  beforeEach(async () => {
    const module: TestingModule =
      await Test.createTestingModule({
        providers: [UsersResolver, UsersService],
      }).compile();

    resolver = module.get<UsersResolver>(UsersResolver);
  });

  it('should create a user with correct types', async () => {
    const input = {
      email: 'test@example.com',
      name: 'Test User',
    };

    const result = await resolver.createUser(input);

    // TypeScriptにより型安全性が保証される
    expect(result.id).toBeDefined();
    expect(result.email).toBe(input.email);
    expect(result.name).toBe(input.name);
    expect(result.createdAt).toBeInstanceOf(Date);
  });
});

まとめ

得られた効果と今後の展望

NestJS と GraphQL を組み合わせた型安全な API 開発により、以下の効果が得られました。

開発効率の大幅向上が実現できます。型定義の二重管理が不要になり、自動生成される型定義により開発者はビジネスロジックに集中できます。

品質の向上も顕著に現れます。コンパイル時の型チェックにより、ランタイムエラーが大幅に減少し、リファクタリングも安全に実行できます。

チーム開発の効率化により、型定義が自動で共有されるため、チームメンバー間での API 仕様の認識齟齬が解消されます。

今後の展望として、以下の発展が期待されます。

エコシステムの拡充により、NestJS と GraphQL の統合がさらに進化し、より多くのツールやライブラリが連携するようになるでしょう。

パフォーマンスの最適化では、DataLoader やキャッシュ機能の統合により、大規模アプリケーションでの性能向上が図られます。

開発体験の向上として、IDE 統合やデバッグツールの改善により、開発者の生産性がさらに向上することが期待されます。

以下の表は、従来の REST API と NestJS + GraphQL アプローチの比較をまとめたものです。

項目REST APINestJS + GraphQL
型安全性手動管理が必要自動生成により保証
API 仕様管理複数エンドポイント単一スキーマ
開発効率中程度高効率
学習コスト低い中程度
フロントエンド連携手動同期自動生成型定義

NestJS と GraphQL の組み合わせは、現代の Web 開発における型安全性と開発効率の両立を実現する優れたソリューションです。初期の学習コストはありますが、中長期的な開発効率と品質向上を考えると、非常に有効な技術選択といえるでしょう。

関連リンク

公式ドキュメント

参考記事