この記事の要点
• NestJSのモジュール・コントローラ・サービスの三層構造を理解する
• 依存性注入(DI)でテスタブルなコードを書く
• class-validatorでDTOベースの入力バリデーションを実装する
このチュートリアルで学ぶこと
- NestJS CLIによるプロジェクト作成
- モジュール / コントローラ / サービスの役割
- 依存性注入(DI)の使い方
- DTOとclass-validatorによる入力バリデーション
- 例外フィルタとパイプの基礎
- シンプルなRESTful API(CRUD)の実装
- ユニットテストの書き方
NestJSはExpressやFastifyの上に構築された、TypeScriptファーストのサーバーサイドフレームワークです。Angular風のデコレータベースの設計を採用し、大規模アプリケーションでも保守しやすい構造を提供します。
前提条件・必要な環境
- Node.js 20以上 (LTS推奨)
- npm 10以上 もしくは pnpm / yarn
- TypeScriptの基礎知識
- ターミナル(macOS / Linux / WSL2)
- 任意: VS Code + ESLint拡張
確認コマンド:
node -v
npm -v
インストール / セットアップ
NestJS CLIをグローバルにインストールし、新しいプロジェクトを作成します。
npm install -g @nestjs/cli
nest new task-api
パッケージマネージャを聞かれるので npm を選択。生成後、プロジェクトに入ってサーバーを起動します。
cd task-api
npm run start:dev
ブラウザで http://localhost:3000 にアクセスすると Hello World! が表示されればOKです。
生成されるディレクトリ構成
task-api/
├── src/
│ ├── app.controller.ts
│ ├── app.controller.spec.ts
│ ├── app.module.ts
│ ├── app.service.ts
│ └── main.ts
├── test/
├── tsconfig.json
└── package.json
基本概念
ポイント: NestJSはModule→Controller→Serviceの三層構造が基本です。この設計パターンを守ることでテスタブルなコードになります。
NestJSの中核となる3つの構成要素を理解しましょう。
Module(モジュール)
機能の単位。関連するコントローラやサービスをまとめます。@Module() デコレータで宣言します。
Controller(コントローラ)
HTTPリクエストを受け取り、適切なサービスへ処理を委譲するクラス。@Controller() で宣言し、@Get() @Post() などでルーティングします。
Provider / Service(プロバイダ)
ビジネスロジックを担当するクラス。@Injectable() を付けることでDIコンテナに登録され、コンストラクタ経由で他クラスに注入できます。
依存性注入(DI)
NestJSはコンストラクタ引数の型を見て自動的にインスタンスを解決します。テストではモックに差し替えやすくなります。
ステップバイステップ実装
ここからは、シンプルなタスク管理APIを作りながら学びます。
Step 1: タスク機能用モジュールを生成
nest generate module tasks
nest generate controller tasks
nest generate service tasks
src/tasks/ 配下に tasks.module.ts tasks.controller.ts tasks.service.ts が生成されます。
Step 2: タスクのモデル定義
src/tasks/task.entity.ts を作成します。
export interface Task {
id: string;
title: string;
description: string;
done: boolean;
createdAt: Date;
}
Step 3: DTOと入力バリデーション
実践メモ: class-validatorのデコレータ(@IsString()、@IsNotEmpty()等)をDTOに付けるだけで自動バリデーションが有効になります。
class-validator を導入します。
npm install class-validator class-transformer
src/tasks/dto/create-task.dto.ts:
import { IsBoolean, IsOptional, IsString, Length } from 'class-validator';
export class CreateTaskDto {
@IsString()
@Length(1, 100)
title!: string;
@IsString()
@Length(0, 500)
description: string = '';
@IsBoolean()
@IsOptional()
done?: boolean;
}
src/tasks/dto/update-task.dto.ts:
import { PartialType } from '@nestjs/mapped-types';
import { CreateTaskDto } from './create-task.dto';
export class UpdateTaskDto extends PartialType(CreateTaskDto) {}
@nestjs/mapped-types を入れていない場合は追加します。
npm install @nestjs/mapped-types
Step 4: グローバルパイプを有効化
src/main.ts を編集し、ValidationPipeを全リクエストに適用します。
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}),
);
await app.listen(3000);
}
bootstrap();
Step 5: TasksServiceの実装
src/tasks/tasks.service.ts:
import { Injectable, NotFoundException } from '@nestjs/common';
import { randomUUID } from 'crypto';
import { Task } from './task.entity';
import { CreateTaskDto } from './dto/create-task.dto';
import { UpdateTaskDto } from './dto/update-task.dto';
@Injectable()
export class TasksService {
private tasks: Task[] = [];
findAll(): Task[] {
return this.tasks;
}
findOne(id: string): Task {
const task = this.tasks.find((t) => t.id === id);
if (!task) {
throw new NotFoundException(`Task ${id} not found`);
}
return task;
}
create(dto: CreateTaskDto): Task {
const task: Task = {
id: randomUUID(),
title: dto.title,
description: dto.description ?? '',
done: dto.done ?? false,
createdAt: new Date(),
};
this.tasks.push(task);
return task;
}
update(id: string, dto: UpdateTaskDto): Task {
const task = this.findOne(id);
Object.assign(task, dto);
return task;
}
remove(id: string): void {
const index = this.tasks.findIndex((t) => t.id === id);
if (index === -1) {
throw new NotFoundException(`Task ${id} not found`);
}
this.tasks.splice(index, 1);
}
}
Step 6: TasksControllerの実装
src/tasks/tasks.controller.ts:
import {
Body,
Controller,
Delete,
Get,
HttpCode,
Param,
Patch,
Post,
} from '@nestjs/common';
import { TasksService } from './tasks.service';
import { CreateTaskDto } from './dto/create-task.dto';
import { UpdateTaskDto } from './dto/update-task.dto';
@Controller('tasks')
export class TasksController {
constructor(private readonly tasksService: TasksService) {}
@Get()
findAll() {
return this.tasksService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.tasksService.findOne(id);
}
@Post()
create(@Body() dto: CreateTaskDto) {
return this.tasksService.create(dto);
}
@Patch(':id')
update(@Param('id') id: string, @Body() dto: UpdateTaskDto) {
return this.tasksService.update(id, dto);
}
@Delete(':id')
@HttpCode(204)
remove(@Param('id') id: string) {
this.tasksService.remove(id);
}
}
Step 7: モジュールに登録
src/tasks/tasks.module.ts:
import { Module } from '@nestjs/common';
import { TasksController } from './tasks.controller';
import { TasksService } from './tasks.service';
@Module({
controllers: [TasksController],
providers: [TasksService],
})
export class TasksModule {}
src/app.module.ts にimport:
import { Module } from '@nestjs/common';
import { TasksModule } from './tasks/tasks.module';
@Module({
imports: [TasksModule],
})
export class AppModule {}
Step 8: 動作確認
サーバーを起動し、curlでAPIを叩きます。
npm run start:dev
curl -X POST http://localhost:3000/tasks \
-H 'Content-Type: application/json' \
-d '{"title":"買い物","description":"牛乳を買う"}'
curl http://localhost:3000/tasks
curl -X PATCH http://localhost:3000/tasks/<id> \
-H 'Content-Type: application/json' \
-d '{"done":true}'
curl -X DELETE http://localhost:3000/tasks/<id>
不正な入力を送ると、ValidationPipeが400を返してくれます。
curl -X POST http://localhost:3000/tasks \
-H 'Content-Type: application/json' \
-d '{"title":""}'
# => 400 Bad Request: title must be longer than or equal to 1 characters
Step 9: ユニットテスト
src/tasks/tasks.service.spec.ts:
import { Test, TestingModule } from '@nestjs/testing';
import { TasksService } from './tasks.service';
import { NotFoundException } from '@nestjs/common';
describe('TasksService', () => {
let service: TasksService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [TasksService],
}).compile();
service = module.get<TasksService>(TasksService);
});
it('creates and returns a task', () => {
const task = service.create({ title: 'test', description: '' });
expect(task.id).toBeDefined();
expect(service.findAll()).toHaveLength(1);
});
it('throws when task not found', () => {
expect(() => service.findOne('missing')).toThrow(NotFoundException);
});
it('updates a task', () => {
const task = service.create({ title: 'a', description: '' });
const updated = service.update(task.id, { done: true });
expect(updated.done).toBe(true);
});
it('removes a task', () => {
const task = service.create({ title: 'a', description: '' });
service.remove(task.id);
expect(service.findAll()).toHaveLength(0);
});
});
実行:
npm test
完成コード全体
最終的なディレクトリ構造は以下のようになります。
task-api/
├── src/
│ ├── tasks/
│ │ ├── dto/
│ │ │ ├── create-task.dto.ts
│ │ │ └── update-task.dto.ts
│ │ ├── task.entity.ts
│ │ ├── tasks.controller.ts
│ │ ├── tasks.module.ts
│ │ ├── tasks.service.ts
│ │ └── tasks.service.spec.ts
│ ├── app.module.ts
│ └── main.ts
├── package.json
└── tsconfig.json
各ファイルは前のステップで示したコードをそのまま配置すれば動作します。
よくあるエラーと対処
Error: Nest can’t resolve dependencies of the TasksController
TasksService がモジュールの providers に登録されていないと発生します。tasks.module.ts の providers 配列を確認してください。
Error: Cannot find module ‘@nestjs/mapped-types’
PartialType を使用するため、別パッケージのインストールが必要です。
npm install @nestjs/mapped-types
バリデーションが効かない
注意: ValidationPipeをグローバル設定し忘れると、DTOのデコレータが機能しません。main.tsのuseGlobalPipesを必ず確認してください。
ValidationPipe をグローバル設定し忘れているケースが大半です。main.ts の useGlobalPipes を確認しましょう。また、DTOにデコレータを正しく付けているかも要チェックです。
TypeScriptで「Property has no initializer」エラー
tsconfig.json の strictPropertyInitialization が有効な場合、! (definite assignment assertion) もしくは初期値が必要です。
ベストプラクティス
- 層を意識する: Controllerはルーティングのみ、ビジネスロジックはServiceに、永続化はRepository層に分離する
- DTOは必ず使う: バリデーションだけでなく、APIの契約として機能させる
- 例外はNestJSの組み込み例外を使う:
NotFoundExceptionBadRequestExceptionなどを使うと、HTTPステータスが自動で適切になる - 設定は @nestjs/config で管理: 環境変数を型安全に扱える
- モジュールは機能単位で分割: AppModuleが肥大化しないように
- Logger を活用:
Loggerクラスでコンテキスト付きログを出力する - テストはServiceから書く: Controllerは薄く保ち、ロジックを集中させると単体テストが書きやすい
次のステップ(発展課題)
- TypeORM もしくは Prisma を導入してデータをDBに永続化する
- 認証を追加:
@nestjs/passport+ JWTでログイン機能を作る - Swaggerドキュメント自動生成:
@nestjs/swaggerを追加 - Guards / Interceptorsを学んでロール制御やレスポンス変換を実装
- キュー処理:
@nestjs/bullでバックグラウンドジョブ - マイクロサービス化: gRPCやNATSトランスポートを使ってみる
- E2Eテスト:
supertest+ Test moduleで実際のHTTPを叩くテストを書く
まとめ
NestJSは「TypeScriptで本格的なバックエンドを書きたい」「Express直書きでは構造が破綻する」というニーズに最適です。
- モジュール / コントローラ / サービスの3層構造でコードが整理しやすい
- 依存性注入によりテスタブルな設計が自然と身につく
- DTO + ValidationPipeで型安全な入力検証が手軽
- CLIによりボイラープレートを自動生成でき、開発スピードが速い
このチュートリアルで作ったTasks APIは、実プロダクトのベースとしても十分実用的です。次はDB接続と認証を追加して、本格的なWebサービスへ拡張してみてください。
参考リソース
- NestJS Official Documentation
- NestJS GitHub Repository
- NestJS First Steps Guide
- class-validator GitHub