T-CREATOR

【実践】NestJS で REST API を構築する基本的な流れ

【実践】NestJS で REST API を構築する基本的な流れ

Node.js での API 開発において、大規模なアプリケーションを構築する際に構造化された設計が求められています。

NestJS は Angular にインスパイアされた Node.js フレームワークで、TypeScript を標準サポートし、依存性注入やモジュール設計により保守性の高い REST API を構築できます。本記事では、NestJS を使って実際に REST API を構築する流れを、初心者の方にもわかりやすく解説いたします。

背景

NestJS とは

NestJS は 2017 年にリリースされた比較的新しい Node.js フレームワークです。Angular の設計哲学を Node.js 環境に持ち込み、エンタープライズレベルのサーバーサイドアプリケーション開発を効率化することを目的としています。

mermaidflowchart TB
    angular[Angular] -->|設計思想| nestjs[NestJS]
    express[Express.js] -->|基盤| nestjs
    typescript[TypeScript] -->|標準サポート| nestjs
    decorator[デコレータ] -->|機能実装| nestjs
    nestjs --> api[スケーラブルなAPI]

NestJS の特徴として、TypeScript ファーストの開発体験、豊富なデコレータによる宣言的な記述、そして強力な依存性注入システムが挙げられます。

REST API 開発における NestJS の位置づけ

現代の Web API 開発では、単純な CRUD 操作を超えて、認証・認可、バリデーション、ドキュメント化、テストなど多岐にわたる要件が求められます。NestJS はこれらの機能を標準で提供し、開発者がビジネスロジックに集中できる環境を提供しています。

#機能分野Express.jsNestJS
1基本的なルーティング標準機能豊富なデコレータで宣言的
2認証・認可追加パッケージが必要Passport 統合、Guard システム
3バリデーション手動実装class-validator との統合
4API ドキュメント別途設定Swagger 自動生成
5テストJest など別途設定テスト機能内蔵

従来の Node.js 開発との比較

Express.js を使った従来の開発では、プロジェクトが成長するにつれて以下のような課題が浮上してきました。

mermaidflowchart LR
    express[Express.js] --> issues[課題]
    issues --> structure[構造化の困難]
    issues --> test[テストの複雑化]
    issues --> maintain[保守性の低下]

    nestjs[NestJS] --> solutions[解決策]
    solutions --> modules[モジュール設計]
    solutions --> di[依存性注入]
    solutions --> builtin[豊富な標準機能]

従来のアプローチでは、開発者が独自にファイル構造を決定し、依存関係を管理する必要がありました。一方、NestJS は規約に基づいた構造化されたアプローチを提供し、チーム開発での一貫性を保ちます。

課題

Express.js での開発の限界

Express.js は軽量で柔軟性が高い反面、大規模なアプリケーション開発において以下のような制約が生じがちです。

まず、ファイル構造の統一性に関する課題があります。Express.js では特定のプロジェクト構造を強制しないため、チームメンバーによって実装方法が異なり、コードの一貫性を保つのが困難になります。

javascript// 従来のExpress.jsでの実装例
app.get('/users', (req, res) => {
  // ビジネスロジック、バリデーション、エラーハンドリングがすべて混在
  if (!req.query.page) {
    return res
      .status(400)
      .json({ error: 'Page parameter required' });
  }

  UserService.getUsers(req.query.page)
    .then((users) => res.json(users))
    .catch((err) =>
      res.status(500).json({ error: err.message })
    );
});

このように、ルーティング、バリデーション、ビジネスロジックが一箇所に集約され、保守性が低下します。

スケーラブルな API 設計の難しさ

API が成長するにつれて、以下のような設計上の問題が顕在化します。

依存関係の管理が複雑になることです。サービス間の依存関係を手動で管理する必要があり、テストやリファクタリングが困難になります。

javascript// 手動での依存関係管理の例
const userService = new UserService(
  new DatabaseConnection(),
  new Logger()
);
const authService = new AuthService(
  userService,
  new JWTProvider()
);

横断的関心事の実装も課題となります。認証、ログ、バリデーションなどの機能を各エンドポイントで個別に実装する必要があり、コードの重複が発生します。

TypeScript との統合課題

Express.js で TypeScript を使用する場合、以下のような問題が生じることがあります。

型安全性の部分的な確保しかできません。リクエスト・レスポンスの型定義や、ミドルウェアでの型の連携が不完全になりがちです。

typescript// Express.jsでのTypeScript使用時の型安全性の課題
app.get('/users/:id', (req: Request, res: Response) => {
  // req.params.idの型が string | undefined で不確実
  // req.bodyの型情報が不十分
  const userId = parseInt(req.params.id); // 型変換が必要
});

デコレータの活用が限定的で、メタデータベースの機能実装が困難になります。

解決策

NestJS の特徴とメリット

NestJS は前述の課題を以下の特徴により解決します。

アーキテクチャの統一により、プロジェクト全体で一貫した構造を提供します。コントローラー、サービス、モジュールという明確な役割分担により、保守性の高いコードが実現できます。

mermaidflowchart TD
    module[Module] --> controller[Controller]
    module --> service[Service]
    module --> provider[Provider]
    controller -->|依存性注入| service
    service -->|ビジネスロジック| db[(Database)]
    controller -->|HTTP レスポンス| client[Client]

TypeScript ファーストの設計により、型安全性が標準で保証されます。リクエスト・レスポンスの型定義から、依存性注入まで、すべて型チェックの恩恵を受けられます。

依存性注入とモジュール設計

NestJS の最大の特徴の一つが**依存性注入(DI: Dependency Injection)**です。これにより、オブジェクト間の依存関係をフレームワークが自動で解決し、テスタブルで疎結合なコードが実現できます。

typescript// NestJSでの依存性注入の例
@Controller('users')
export class UsersController {
  constructor(
    private readonly usersService: UsersService, // 自動で注入される
    private readonly authService: AuthService // 自動で注入される
  ) {}
}

モジュールシステムにより、機能ごとにコードを整理し、再利用可能なコンポーネントを作成できます。

mermaidflowchart TB
    app[AppModule] --> users[UsersModule]
    app --> auth[AuthModule]
    app --> common[CommonModule]

    users --> usersController[UsersController]
    users --> usersService[UsersService]

    auth --> authController[AuthController]
    auth --> authService[AuthService]

デコレータベースの開発手法

NestJS はデコレータを活用した宣言的な開発スタイルを採用しています。これにより、コードの可読性が向上し、横断的関心事の実装が簡潔になります。

ルーティングデコレータにより、HTTP メソッドとパスを明確に定義できます。

typescript@Controller('users')
export class UsersController {
  @Get()           // GET /users
  @Post()          // POST /users
  @Put(':id')      // PUT /users/:id
  @Delete(':id')   // DELETE /users/:id
}

バリデーションデコレータにより、入力値の検証を宣言的に記述できます。

typescriptexport class CreateUserDto {
  @IsEmail()
  @IsNotEmpty()
  email: string;

  @IsString()
  @MinLength(8)
  password: string;
}

具体例

環境構築とプロジェクト初期化

NestJS プロジェクトの作成から始めましょう。まず、NestJS CLI をグローバルにインストールします。

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

新しいプロジェクトを作成します。CLI が対話形式でセットアップを進めてくれます。

bash# プロジェクト作成
nest new my-api-project

# プロジェクトディレクトリに移動
cd my-api-project

プロジェクトの基本構造を確認しましょう。

textmy-api-project/
├── src/
│   ├── app.controller.ts      # メインコントローラー
│   ├── app.service.ts         # メインサービス
│   ├── app.module.ts          # ルートモジュール
│   └── main.ts                # アプリケーションエントリーポイント
├── test/                      # テストファイル
├── package.json
└── tsconfig.json

開発サーバーを起動し、動作確認を行います。

bash# 開発サーバー起動
yarn start:dev

ブラウザで http:​/​​/​localhost:3000 にアクセスすると、"Hello World!" が表示されます。これで基本的な環境構築が完了です。

Controller の作成

実際のユーザー管理 API を作成しましょう。まず、ユーザー関連のリソースを生成します。

bash# ユーザーモジュール、コントローラー、サービスを一括生成
nest generate resource users

CLI が対話形式で以下を確認します:

  • Transport layer: REST API を選択
  • CRUD entry points: Yes を選択

生成された users.controller.ts を確認します。

typescript// src/users/users.controller.ts
import {
  Controller,
  Get,
  Post,
  Body,
  Patch,
  Param,
  Delete,
} from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';

@Controller('users')
export class UsersController {
  constructor(
    private readonly usersService: UsersService
  ) {}

  @Post()
  create(@Body() createUserDto: CreateUserDto) {
    return this.usersService.create(createUserDto);
  }

  @Get()
  findAll() {
    return this.usersService.findAll();
  }

  @Get(':id')
  findOne(@Param('id') id: string) {
    return this.usersService.findOne(+id);
  }
}

各デコレータの役割を理解しましょう。

#デコレータ説明HTTP メソッド
1@Controller('users')クラス全体のベースパスを設定-
2@Post()POST リクエストを処理POST
3@Get()GET リクエストを処理GET
4@Get(':id')パラメータ付き GET リクエストGET
5@Body()リクエストボディを取得-
6@Param('id')パスパラメータを取得-

Service の実装

ビジネスロジックを担当するサービスを実装します。生成された users.service.ts を編集しましょう。

typescript// src/users/users.service.ts
import {
  Injectable,
  NotFoundException,
} from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';

// ユーザーエンティティの型定義
export interface User {
  id: number;
  email: string;
  name: string;
  createdAt: Date;
}

@Injectable()
export class UsersService {
  private users: User[] = []; // 仮のデータストア
  private currentId = 1;

  create(createUserDto: CreateUserDto): User {
    const newUser: User = {
      id: this.currentId++,
      ...createUserDto,
      createdAt: new Date(),
    };

    this.users.push(newUser);
    return newUser;
  }

  findAll(): User[] {
    return this.users;
  }

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

@Injectable() デコレータにより、このサービスが依存性注入の対象となり、コントローラーで自動的に注入されます。

DTO とバリデーション

データ転送オブジェクト(DTO)とバリデーション機能を実装します。まず、必要なパッケージをインストールします。

bash# バリデーション用パッケージのインストール
yarn add class-validator class-transformer

create-user.dto.ts を編集してバリデーションルールを追加します。

typescript// src/users/dto/create-user.dto.ts
import {
  IsEmail,
  IsNotEmpty,
  IsString,
  MinLength,
  MaxLength,
} from 'class-validator';

export class CreateUserDto {
  @IsEmail(
    {},
    { message: '有効なメールアドレスを入力してください' }
  )
  @IsNotEmpty({ message: 'メールアドレスは必須です' })
  email: string;

  @IsString({ message: '名前は文字列である必要があります' })
  @IsNotEmpty({ message: '名前は必須です' })
  @MinLength(2, {
    message: '名前は2文字以上である必要があります',
  })
  @MaxLength(50, {
    message: '名前は50文字以下である必要があります',
  })
  name: string;
}

グローバルバリデーションパイプを設定します。

typescript// src/main.ts
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // グローバルバリデーションパイプの設定
  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true, // DTOで定義されていないプロパティを除外
      forbidNonWhitelisted: true, // 不正なプロパティがある場合エラー
      transform: true, // 型変換を自動実行
    })
  );

  await app.listen(3000);
}
bootstrap();

データベース連携(TypeORM)

実際のデータベースと連携しましょう。TypeORM を使用して PostgreSQL に接続します。

bash# TypeORMとデータベース関連パッケージのインストール
yarn add @nestjs/typeorm typeorm pg
yarn add -D @types/pg

ユーザーエンティティを作成します。

typescript// src/users/entities/user.entity.ts
import {
  Entity,
  Column,
  PrimaryGeneratedColumn,
  CreateDateColumn,
  UpdateDateColumn,
} from 'typeorm';

@Entity('users')
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ unique: true })
  email: string;

  @Column()
  name: string;

  @CreateDateColumn()
  createdAt: Date;

  @UpdateDateColumn()
  updatedAt: Date;
}

データベース接続を設定します。

typescript// src/app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersModule } from './users/users.module';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'postgres',
      host: 'localhost',
      port: 5432,
      username: 'your-username',
      password: 'your-password',
      database: 'your-database',
      autoLoadEntities: true,
      synchronize: true, // 本番環境では false に設定
    }),
    UsersModule,
  ],
})
export class AppModule {}

ユーザーモジュールでエンティティを登録します。

typescript// src/users/users.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
import { User } from './entities/user.entity';

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  controllers: [UsersController],
  providers: [UsersService],
})
export class UsersModule {}

サービスをデータベース操作に対応させます。

typescript// src/users/users.service.ts(データベース対応版)
import {
  Injectable,
  NotFoundException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CreateUserDto } from './dto/create-user.dto';
import { User } from './entities/user.entity';

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private usersRepository: Repository<User>
  ) {}

  async create(
    createUserDto: CreateUserDto
  ): Promise<User> {
    const user = this.usersRepository.create(createUserDto);
    return await this.usersRepository.save(user);
  }

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

  async findOne(id: number): Promise<User> {
    const user = await this.usersRepository.findOne({
      where: { id },
    });
    if (!user) {
      throw new NotFoundException(
        `User with ID ${id} not found`
      );
    }
    return user;
  }
}

認証機能の実装

JWT を使った認証機能を実装します。まず、必要なパッケージをインストールします。

bash# 認証関連パッケージのインストール
yarn add @nestjs/jwt @nestjs/passport passport passport-jwt bcryptjs
yarn add -D @types/passport-jwt @types/bcryptjs

認証モジュールを生成します。

bash# 認証モジュール生成
nest generate module auth
nest generate service auth
nest generate controller auth

JWT 設定を追加します。

typescript// src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { UsersModule } from '../users/users.module';
import { JwtStrategy } from './jwt.strategy';

@Module({
  imports: [
    UsersModule,
    PassportModule,
    JwtModule.register({
      secret: 'your-secret-key', // 環境変数から読み込むことを推奨
      signOptions: { expiresIn: '24h' },
    }),
  ],
  providers: [AuthService, JwtStrategy],
  controllers: [AuthController],
})
export class AuthModule {}

JWT ストラテジーを実装します。

typescript// src/auth/jwt.strategy.ts
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';

@Injectable()
export class JwtStrategy extends PassportStrategy(
  Strategy
) {
  constructor() {
    super({
      jwtFromRequest:
        ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: 'your-secret-key',
    });
  }

  async validate(payload: any) {
    return { userId: payload.sub, email: payload.email };
  }
}

認証が必要なエンドポイントに Guard を適用します。

typescript// src/users/users.controller.ts(認証対応版)
import {
  Controller,
  Get,
  Post,
  Body,
  UseGuards,
} from '@nestjs/common';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { UsersService } from './users.service';

@Controller('users')
@UseGuards(JwtAuthGuard) // 全エンドポイントに認証を適用
export class UsersController {
  constructor(
    private readonly usersService: UsersService
  ) {}

  @Get()
  findAll() {
    return this.usersService.findAll();
  }
}

まとめ

NestJS を使った REST API 開発は、従来の Express.js ベースの開発と比較して以下のメリットがあります。

まず、構造化されたアーキテクチャにより、大規模なプロジェクトでも一貫性を保った開発が可能です。モジュール設計と依存性注入により、テストしやすく保守しやすいコードが実現できます。

TypeScript ファーストの設計により、型安全性が保証され、開発時のエラーを早期に発見できます。また、豊富な標準機能により、認証、バリデーション、データベース操作などの実装が大幅に簡素化されます。

デコレータベースの開発手法により、コードの可読性が向上し、横断的関心事の実装が宣言的に記述できます。これにより、ビジネスロジックに集中した開発が可能になります。

NestJS は学習コストが高い面もありますが、チーム開発や長期的な保守性を考慮した場合、その投資に見合う価値を提供してくれるフレームワークです。

今回紹介した基本的な構築方法をベースに、実際のプロジェクトでの要件に合わせてカスタマイズしていくことで、スケーラブルで保守しやすい API を構築できるでしょう。

関連リンク