NestJS: 本格的なREST APIの構築ガイド
NestJSで本格的なREST APIを構築するための完全ガイドです。コントローラー、サービス、モジュール構成、class-validatorによるバリデーション、エラーハンドリングを実践的に解説します。

NestJSは、スケーラブルで保守性の高いサーバーサイドアプリケーションを構築するためのNode.jsフレームワークとして確固たる地位を確立しています。Angularにインスパイアされたモジュラーアーキテクチャ、依存性注入(DI)、そしてネイティブなTypeScriptサポートにより、本格的なREST APIの構築が体系的かつ予測可能になります。
NestJS 11では、パフォーマンスの大幅な改善、グレースフルシャットダウンのためのネイティブシグナルサポート、PrismaやDrizzleなどのモダンORMとの統合の簡素化が導入されています。以前のバージョンとの後方互換性も維持されています。
プロジェクトのインストールと初期設定
NestJS CLIを使用すると、本番環境に対応した構成のプロジェクトが自動生成されます。TypeScript、ESLint、ユニットテストが自動的に設定されます。
# terminal
# Global installation of the NestJS CLI
npm install -g @nestjs/cli
# Create a new project
nest new my-api
# Navigate to the project
cd my-api
# Start in development mode with hot-reload
npm run start:devこのコマンドにより、整理されたファイル構造と必要な依存関係がプリインストールされたプロジェクトが作成されます。
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
// Create the NestJS application
const app = await NestFactory.create(AppModule);
// Enable global validation
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // Removes non-decorated properties
forbidNonWhitelisted: true, // Rejects requests with unknown properties
transform: true, // Transforms payloads to DTO instances
}));
// Configure global prefix for all routes
app.setGlobalPrefix('api');
await app.listen(3000);
}
bootstrap();この設定により、リクエストの自動バリデーションが有効になり、すべてのルートに /api プレフィックスが付与されます。
モジュラーアーキテクチャの理解
NestJSはコードをモジュール単位で整理し、各モジュールが機能ドメインをカプセル化します。モジュールはコントローラー(HTTPエンドポイント)、プロバイダー(ビジネスサービス)、インポート(依存関係)を宣言します。
import { Module } from '@nestjs/common';
import { UsersModule } from './users/users.module';
import { ProductsModule } from './products/products.module';
import { AuthModule } from './auth/auth.module';
// The root module imports all application modules
@Module({
imports: [
UsersModule, // User management
ProductsModule, // Product catalog
AuthModule, // Authentication
],
})
export class AppModule {}各ビジネスモジュールは同じ構造に従います。モジュールファイル、コントローラー、そしてサービスの3つで構成されます。
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
@Module({
// Controllers handle HTTP requests
controllers: [UsersController],
// Providers are injectable throughout the module
providers: [UsersService],
// Exports make providers available to other modules
exports: [UsersService],
})
export class UsersModule {}UsersService をエクスポートすることで、他のモジュールが UsersModule をインポートし、依存性注入を通じてこのサービスを利用できるようになります。
完全なCRUDコントローラーの構築
コントローラーはHTTPルートを定義し、ビジネスロジックをサービスに委譲します。NestJSのデコレーターにより、コードは表現力豊かで自己文書化されたものになります。
import {
Controller,
Get,
Post,
Put,
Delete,
Body,
Param,
Query,
HttpCode,
HttpStatus,
ParseIntPipe,
} from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { User } from './entities/user.entity';
// Route prefix: /api/users
@Controller('users')
export class UsersController {
// Inject service via constructor
constructor(private readonly usersService: UsersService) {}
// POST /api/users - Create a user
@Post()
@HttpCode(HttpStatus.CREATED)
async create(@Body() createUserDto: CreateUserDto): Promise<User> {
// The DTO is automatically validated before reaching here
return this.usersService.create(createUserDto);
}
// GET /api/users - List with pagination
@Get()
async findAll(
@Query('page', new ParseIntPipe({ optional: true })) page: number = 1,
@Query('limit', new ParseIntPipe({ optional: true })) limit: number = 10,
): Promise<{ data: User[]; total: number }> {
return this.usersService.findAll(page, limit);
}
// GET /api/users/:id - Retrieve by ID
@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number): Promise<User> {
// ParseIntPipe automatically converts and validates the parameter
return this.usersService.findOne(id);
}
// PUT /api/users/:id - Full update
@Put(':id')
async update(
@Param('id', ParseIntPipe) id: number,
@Body() updateUserDto: UpdateUserDto,
): Promise<User> {
return this.usersService.update(id, updateUserDto);
}
// DELETE /api/users/:id - Deletion
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
async remove(@Param('id', ParseIntPipe) id: number): Promise<void> {
await this.usersService.remove(id);
}
}ParseIntPipe などのパイプは、パラメータの変換とバリデーションを自動的に行います。変換に失敗した場合、HTTP 400エラーが自動的に返されます。
NestJSには ParseIntPipe、ParseBoolPipe、ParseArrayPipe、ParseUUIDPipe など、複数の組み込みパイプが用意されています。各パイプはハンドラーの実行前に受信データのバリデーションと変換を行います。
ビジネスサービスの実装
サービスはビジネスロジックとデータアクセスをカプセル化します。@Injectable() デコレーターでマークされたサービスは、NestJSの依存性注入コンテナによって管理されます。
import { Injectable, NotFoundException } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { User } from './entities/user.entity';
@Injectable()
export class UsersService {
// In-memory database simulation
private users: User[] = [];
private idCounter = 1;
async create(createUserDto: CreateUserDto): Promise<User> {
// Create the user entity
const user: User = {
id: this.idCounter++,
...createUserDto,
createdAt: new Date(),
updatedAt: new Date(),
};
this.users.push(user);
return user;
}
async findAll(page: number, limit: number): Promise<{ data: User[]; total: number }> {
// Calculate pagination
const start = (page - 1) * limit;
const end = start + limit;
return {
data: this.users.slice(start, end),
total: this.users.length,
};
}
async findOne(id: number): Promise<User> {
const user = this.users.find(u => u.id === id);
// Throw exception if user doesn't exist
if (!user) {
throw new NotFoundException(`User with ID ${id} not found`);
}
return user;
}
async update(id: number, updateUserDto: UpdateUserDto): Promise<User> {
const user = await this.findOne(id);
// Merge existing data with updates
Object.assign(user, updateUserDto, { updatedAt: new Date() });
return user;
}
async remove(id: number): Promise<void> {
const index = this.users.findIndex(u => u.id === id);
if (index === -1) {
throw new NotFoundException(`User with ID ${id} not found`);
}
this.users.splice(index, 1);
}
// Utility method for other services
async findByEmail(email: string): Promise<User | undefined> {
return this.users.find(u => u.email === email);
}
}NotFoundException 例外は、フォーマットされたエラーメッセージとともにHTTP 404レスポンスを自動的に生成します。
Node.js / NestJSの面接対策はできていますか?
インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。
class-validatorによるデータバリデーション
DTO(Data Transfer Object)は受信データの構造を定義します。class-validator のデコレーターでバリデーションルールを指定すると、自動的に適用されます。
import {
IsEmail,
IsNotEmpty,
IsString,
MinLength,
MaxLength,
IsOptional,
Matches,
} from 'class-validator';
export class CreateUserDto {
@IsNotEmpty({ message: 'Email is required' })
@IsEmail({}, { message: 'Invalid email format' })
email: string;
@IsNotEmpty({ message: 'Password is required' })
@MinLength(8, { message: 'Password must be at least 8 characters' })
@MaxLength(50, { message: 'Password cannot exceed 50 characters' })
@Matches(
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/,
{ message: 'Password must contain at least one uppercase, one lowercase, and one number' }
)
password: string;
@IsNotEmpty({ message: 'First name is required' })
@IsString()
@MinLength(2)
@MaxLength(50)
firstName: string;
@IsNotEmpty({ message: 'Last name is required' })
@IsString()
@MinLength(2)
@MaxLength(50)
lastName: string;
@IsOptional()
@IsString()
@MaxLength(20)
phone?: string;
}部分更新には PartialType を使用します。バリデーションルールを保持したまま、すべてのフィールドがオプションになります。
import { PartialType, OmitType } from '@nestjs/mapped-types';
import { CreateUserDto } from './create-user.dto';
// All fields from CreateUserDto become optional
// Password is excluded from standard updates
export class UpdateUserDto extends PartialType(
OmitType(CreateUserDto, ['password'] as const)
) {}エンティティは保存されるデータの構造を表現します。
export class User {
id: number;
email: string;
password: string;
firstName: string;
lastName: string;
phone?: string;
createdAt: Date;
updatedAt: Date;
}エラーハンドリングの一元管理
NestJSにはHTTP例外が組み込まれています。グローバル例外フィルターを使用することで、エラーレスポンスのフォーマットをカスタマイズできます。
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
HttpStatus,
} from '@nestjs/common';
import { Request, Response } from 'express';
// Catches all HttpExceptions
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status = exception.getStatus();
// Retrieve error message (can be string or object)
const exceptionResponse = exception.getResponse();
const message = typeof exceptionResponse === 'string'
? exceptionResponse
: (exceptionResponse as any).message;
// Standardized response format
response.status(status).json({
success: false,
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
method: request.method,
message: message,
});
}
}HTTP以外のエラー(システムエラー、未処理の例外)をキャッチするために、2つ目のフィルターで完全なカバレッジを確保します。
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
HttpStatus,
} from '@nestjs/common';
import { Request, Response } from 'express';
// Catches ALL exceptions (including system errors)
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
// Determine HTTP code and message
const status = exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
const message = exception instanceof HttpException
? exception.message
: 'Internal server error';
// Log error for debugging
console.error('Exception caught:', exception);
response.status(status).json({
success: false,
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
message: message,
});
}
}メインモジュールでグローバルフィルターを登録します。
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
import { HttpExceptionFilter } from './common/filters/http-exception.filter';
import { AllExceptionsFilter } from './common/filters/all-exceptions.filter';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Global validation
app.useGlobalPipes(new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}));
// Global exception filters
app.useGlobalFilters(
new AllExceptionsFilter(),
new HttpExceptionFilter(),
);
app.setGlobalPrefix('api');
await app.listen(3000);
}
bootstrap();フィルターの登録順序は重要です。最初に登録されたフィルターが最後に実行されます。AllExceptionsFilter はフォールバックとして機能させるため、HttpExceptionFilter よりも先に登録する必要があります。
Prisma ORMとの統合
Prismaは自動生成される型付きクライアントを通じて、データベース操作を簡素化します。NestJSとの完全な統合手順を以下に示します。
# terminal
# Install Prisma
npm install prisma @prisma/client
# Initialize Prisma with PostgreSQL
npx prisma init --datasource-provider postgresqlデータスキーマの定義です。
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
email String @unique
password String
firstName String @map("first_name")
lastName String @map("last_name")
phone String?
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// Relations
posts Post[]
@@map("users")
}
model Post {
id Int @id @default(autoincrement())
title String
content String
published Boolean @default(false)
authorId Int @map("author_id")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// Relation to User
author User @relation(fields: [authorId], references: [id])
@@map("posts")
}再利用可能なPrismaモジュールを作成します。
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
// Automatic connection on module startup
async onModuleInit() {
await this.$connect();
}
// Clean disconnection on application shutdown
async onModuleDestroy() {
await this.$disconnect();
}
}import { Global, Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';
// @Global makes the service available throughout the application
@Global()
@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export class PrismaModule {}Prismaを使用するように更新したUsersServiceです。
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { User } from '@prisma/client';
import * as bcrypt from 'bcrypt';
@Injectable()
export class UsersService {
constructor(private readonly prisma: PrismaService) {}
async create(createUserDto: CreateUserDto): Promise<Omit<User, 'password'>> {
// Check email uniqueness
const existingUser = await this.prisma.user.findUnique({
where: { email: createUserDto.email },
});
if (existingUser) {
throw new ConflictException('This email is already in use');
}
// Hash the password
const hashedPassword = await bcrypt.hash(createUserDto.password, 10);
// Create the user
const user = await this.prisma.user.create({
data: {
...createUserDto,
password: hashedPassword,
},
});
// Exclude password from response
const { password, ...result } = user;
return result;
}
async findAll(page: number, limit: number): Promise<{ data: User[]; total: number }> {
// Parallel execution of count and paginated query
const [data, total] = await Promise.all([
this.prisma.user.findMany({
skip: (page - 1) * limit,
take: limit,
orderBy: { createdAt: 'desc' },
select: {
id: true,
email: true,
firstName: true,
lastName: true,
phone: true,
createdAt: true,
updatedAt: true,
},
}),
this.prisma.user.count(),
]);
return { data: data as User[], total };
}
async findOne(id: number): Promise<Omit<User, 'password'>> {
const user = await this.prisma.user.findUnique({
where: { id },
select: {
id: true,
email: true,
firstName: true,
lastName: true,
phone: true,
createdAt: true,
updatedAt: true,
},
});
if (!user) {
throw new NotFoundException(`User with ID ${id} not found`);
}
return user as Omit<User, 'password'>;
}
async update(id: number, updateUserDto: UpdateUserDto): Promise<Omit<User, 'password'>> {
// Check existence
await this.findOne(id);
const user = await this.prisma.user.update({
where: { id },
data: updateUserDto,
select: {
id: true,
email: true,
firstName: true,
lastName: true,
phone: true,
createdAt: true,
updatedAt: true,
},
});
return user as Omit<User, 'password'>;
}
async remove(id: number): Promise<void> {
await this.findOne(id);
await this.prisma.user.delete({ where: { id } });
}
}インターセプターによるレスポンス変換
インターセプターを使用すると、レスポンスを統一的に変換できます。変換インターセプターにより、すべてのAPIレスポンスのフォーマットを標準化します。
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
// Interface for standardized response format
export interface ApiResponse<T> {
success: boolean;
data: T;
timestamp: string;
}
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, ApiResponse<T>> {
intercept(context: ExecutionContext, next: CallHandler): Observable<ApiResponse<T>> {
return next.handle().pipe(
map(data => ({
success: true,
data,
timestamp: new Date().toISOString(),
})),
);
}
}ロギングインターセプターはリクエストとその実行時間を記録します。
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
Logger,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
private readonly logger = new Logger(LoggingInterceptor.name);
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const { method, url } = request;
const now = Date.now();
return next.handle().pipe(
tap(() => {
const response = context.switchToHttp().getResponse();
const { statusCode } = response;
const duration = Date.now() - now;
// Structured log format
this.logger.log(
`${method} ${url} ${statusCode} - ${duration}ms`
);
}),
);
}
}まとめ
NestJSは、本格的なREST APIを構築するための堅牢かつスケーラブルなアーキテクチャを提供します。TypeScript、依存性注入、そして表現力豊かなデコレーターの組み合わせにより、保守性とテスト容易性に優れたアプリケーションの構築が可能になります。
NestJS APIの品質チェックリスト
- 関心の分離を実現するモジュラー構造
- すべての入力に対するclass-validatorによるDTOバリデーション
- ビジネスロジック専用のサービス層
- 例外フィルターによるエラーハンドリングの一元化
- 変換とロギングのためのインターセプター
- データベースアクセスのためのPrisma統合
- ホワイトリストを有効にしたグローバルValidationPipe
- すべてのルートに対する統一的なAPIプレフィックス
今すぐ練習を始めましょう!
面接シミュレーターと技術テストで知識をテストしましょう。
NestJSの強みは、ベストプラクティスへ自然に導くその意見のある構造にあります。依存性注入やレイヤー分離といった実績のあるパターンにより、テスト可能で拡張性のあるコードが生まれ、エンタープライズアプリケーションにも対応できます。
タグ
共有
