はじめに
アプリケーションサービス部の遠藤です!
この記事では、TypeScriptベースのNode.jsフレームワーク「NestJS」を使用して、AWS Lambda + API Gateway上で動作するサーバーレスAPIを構築する方法を詳しく解説します。
実際に以下の機能を持つECサイト風のAPIを構築しながら、NestJSの基本概念からAWSデプロイまでを一通り学べる内容となっています。
構築するAPI機能
- ヘルスチェック API
- ユーザー管理 API(CRUD操作)
- 商品管理 API(在庫管理含む)
- 注文管理 API(在庫連動)
- Swagger UI による API ドキュメント
技術スタック
- フレームワーク: NestJS
- 言語: TypeScript
- ランタイム: Node.js 18
- クラウド: AWS Lambda + API Gateway
- デプロイ: AWS SAM
- ドキュメント: Swagger/OpenAPI
NestJSとは?
NestJSは、効率的でスケーラブルなNode.jsサーバーサイドアプリケーションを構築するためのフレームワークです。宣言型プログラミングのパラダイムを採用し、開発者が「何をしたいか」に集中できる環境を提供します。
NestJSの特徴
TypeScript ファースト
- TypeScriptで書かれており、型安全性を提供
- JavaScriptでも使用可能
宣言型プログラミング
- デコレータベースの宣言的な開発
- 「どのようにするか」ではなく「何をしたいか」に集中
デコレータベース
- Angular風のデコレータを使用
- メタデータ駆動の開発
モジュラーアーキテクチャ
- 機能ごとにモジュール化
- 依存性注入(DI)をサポート
豊富なエコシステム
- Express/Fastify対応
- GraphQL、WebSocket、マイクロサービス対応
宣言型プログラミングの特徴
NestJSでは、複雑な処理も宣言的に記述できます。
// 従来の命令型アプローチ(Express.js) app.get('/users', (req, res) => { // バリデーション処理を手動で実装 if (!req.query.page || isNaN(req.query.page)) { return res.status(400).json({ error: 'Invalid page parameter' }); } // 認証チェックを手動で実装 const token = req.headers.authorization; if (!token || !verifyToken(token)) { return res.status(401).json({ error: 'Unauthorized' }); } // ビジネスロジック const users = getUsersFromDatabase(req.query.page); res.json(users); }); // NestJSの宣言型アプローチ @Controller('users') export class UsersController { @Get() @UseGuards(JwtAuthGuard) // 認証が必要であることを宣言 @ApiOperation({ summary: 'ユーザー一覧を取得' }) async findAll( @Query('page', ParseIntPipe) page: number = 1 // バリデーションを宣言 ): Promise<User[]> { // 「ユーザー一覧を取得したい」ことを宣言 return this.usersService.findAll(page); } }
この宣言型アプローチにより、コードの可読性、保守性、テスタビリティが大幅に向上します。
基本概念
1. モジュール(Module)
アプリケーションの機能をグループ化する単位です。
@Module({ imports: [UsersModule, ProductsModule], controllers: [AppController], providers: [AppService], }) export class AppModule {}
2. コントローラー(Controller)
HTTPリクエストを処理し、レスポンスを返す役割を担います。
@Controller('users') export class UsersController { @Get() findAll(): string { return 'This action returns all users'; } }
3. サービス(Service)
ビジネスロジックを実装するクラスです。
@Injectable() export class UsersService { findAll(): User[] { return this.users; } }
4. DTO(Data Transfer Object)
データの形式を定義し、バリデーションを行います。
export class CreateUserDto { @IsNotEmpty() @IsString() username: string; @IsEmail() email: string; }
NestJSの主要ユースケース
NestJSは様々な場面で活用できる汎用性の高いフレームワークです。以下に主要なユースケースを紹介します。
1. エンタープライズWebアプリケーション
大規模なチーム開発や複雑なビジネスロジックを持つアプリケーションに最適です。
// 複雑な権限管理を宣言的に実装 @Controller('admin/reports') @UseGuards(AuthGuard, RoleGuard) @Roles('admin', 'manager') export class AdminReportsController { @Get('sales') @Permissions('reports:sales:read') @ApiOperation({ summary: '売上レポートを取得' }) async getSalesReport( @Query() query: SalesReportQueryDto, @CurrentUser() user: User, ): Promise<SalesReportDto> { return this.reportsService.generateSalesReport(query, user); } }
適用場面: ERP、CRM、人事管理システム、在庫管理システム
2. マイクロサービスアーキテクチャ
サービス間通信や分散システムの構築に優れています。
@Controller() export class OrdersController { constructor( @Inject('USER_SERVICE') private userService: ClientProxy, @Inject('PAYMENT_SERVICE') private paymentService: ClientProxy, ) {} @Post('orders') async createOrder(@Body() createOrderDto: CreateOrderDto) { // 他のサービスとの連携を宣言的に記述 const user = await this.userService .send('get_user', { id: createOrderDto.userId }) .toPromise(); const payment = await this.paymentService .send('process_payment', { amount: createOrderDto.totalAmount, userId: createOrderDto.userId }) .toPromise(); return this.ordersService.create(createOrderDto); } }
適用場面: ECサイト、金融システム、IoTプラットフォーム、SaaSアプリケーション
3. RESTful API / GraphQL API
モバイルアプリやSPAのバックエンドAPIとして活用できます。
// RESTful API @Controller('api/v1/products') @ApiTags('products') export class ProductsController { @Get() @ApiOperation({ summary: '商品一覧を取得' }) @ApiQuery({ name: 'category', required: false }) async findAll( @Query('category') category?: string, @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number = 1, ): Promise<PaginatedProductsDto> { return this.productsService.findAll({ category, page }); } } // GraphQL API @Resolver(() => Product) export class ProductsResolver { @Query(() => [Product]) @UseGuards(GqlAuthGuard) async products( @Args('filter', { nullable: true }) filter?: ProductFilterInput, ): Promise<Product[]> { return this.productsService.findAll(filter); } }
適用場面: モバイルアプリバックエンド、外部システム連携API、パートナー向けAPI
4. リアルタイムアプリケーション
WebSocketを使用したリアルタイム通信アプリケーションの構築が可能です。
@WebSocketGateway() export class ChatGateway { @SubscribeMessage('send_message') @UseGuards(WsAuthGuard) @UsePipes(new ValidationPipe()) async handleMessage( @MessageBody() data: SendMessageDto, @ConnectedSocket() client: Socket, ): Promise<void> { const message = await this.chatService.createMessage({ ...data, userId: client.data.userId, }); this.server.to(data.roomId).emit('new_message', message); } }
適用場面: チャットアプリ、ライブ配信、リアルタイム協業ツール、ゲームサーバー
5. サーバーレスアプリケーション(今回の実装)
AWS LambdaやAzure Functionsでの実行に対応しています。
// Lambda関数として動作するNestJSアプリケーション export const handler = async ( event: APIGatewayProxyEvent, context: Context, ): Promise<APIGatewayProxyResult> => { const server = await bootstrap(); return server(event, context); };
適用場面: イベント駆動アーキテクチャ、コスト最適化重視のAPI、スケーラブルなバッチ処理
ユースケース選択の指針
✅ NestJSが適している場面
- 大規模・複雑なアプリケーション
- チーム開発(複数の開発者が関わる)
- 長期保守が必要なプロジェクト
- TypeScriptを活用したい場合
- エンタープライズ要件がある場合
- テスタビリティを重視する場合
❌ NestJSが適さない場面
- シンプルな静的サイト
- プロトタイプ・MVPの迅速な開発
- 学習コストを避けたい場合
- 軽量さを最優先する場合
プロジェクトセットアップ
1. 初期設定
# プロジェクト初期化 npm init -y # NestJS関連の依存関係をインストール npm install @nestjs/core @nestjs/common @nestjs/platform-express @nestjs/swagger reflect-metadata rxjs class-validator class-transformer # 開発用依存関係をインストール npm install -D @nestjs/cli @types/node typescript ts-node nodemon # AWS Lambda用の依存関係をインストール npm install aws-lambda @vendia/serverless-express express npm install -D @types/aws-lambda @types/express
2. TypeScript設定
{ "compilerOptions": { "module": "commonjs", "declaration": true, "removeComments": true, "emitDecoratorMetadata": true, "experimentalDecorators": true, "allowSyntheticDefaultImports": true, "target": "ES2020", "sourceMap": true, "outDir": "./dist", "baseUrl": "./", "incremental": true, "skipLibCheck": true } }
3. package.json スクリプト
{ "scripts": { "build": "tsc", "start": "node dist/main.js", "start:dev": "nodemon --exec ts-node src/main.ts", "start:prod": "node dist/main.js" } }
アプリケーション構造の実装
アプリケーションのエントリーファイルは、ローカル開発用とAWS Lambda用で2種類実装します。
1. メインアプリケーションファイル
src/main.ts(ローカル開発用)
import { NestFactory } from '@nestjs/core'; import { ValidationPipe } from '@nestjs/common'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); // CORS設定 app.enableCors(); // バリデーションパイプの設定 app.useGlobalPipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true, })); // Swagger設定 const config = new DocumentBuilder() .setTitle('NestJS API') .setDescription('AWS上で動作するNestJS API') .setVersion('1.0') .addTag('users') .addTag('products') .addTag('orders') .build(); const document = SwaggerModule.createDocument(app, config); SwaggerModule.setup('api', app, document); const port = process.env.PORT || 3000; await app.listen(port); console.log(`Application is running on: http://localhost:${port}`); } bootstrap();
src/lambda.ts(AWS Lambda用)
import { NestFactory } from '@nestjs/core'; import { ValidationPipe } from '@nestjs/common'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { ExpressAdapter } from '@nestjs/platform-express'; import { Context, APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'; import { configure } from '@vendia/serverless-express'; import * as express from 'express'; import { AppModule } from './app.module'; let cachedServer: any; async function bootstrap() { if (!cachedServer) { console.log('Initializing NestJS application...'); const expressApp = express(); const adapter = new ExpressAdapter(expressApp); const app = await NestFactory.create(AppModule, adapter, { logger: ['error', 'warn', 'log'], }); // CORS設定 app.enableCors({ origin: '*', methods: 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS', allowedHeaders: 'Content-Type,Authorization,X-Requested-With', }); // バリデーションパイプの設定 app.useGlobalPipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true, disableErrorMessages: false, })); // Swagger設定 const config = new DocumentBuilder() .setTitle('NestJS API on AWS') .setDescription('AWS Lambda + API Gateway で動作するNestJS API') .setVersion('1.0') .addTag('health', 'ヘルスチェック関連のAPI') .addTag('users', 'ユーザー管理API') .addTag('products', '商品管理API') .addTag('orders', '注文管理API') .build(); const document = SwaggerModule.createDocument(app, config); SwaggerModule.setup('api', app, document, { customSiteTitle: 'NestJS API Documentation', customCss: '.swagger-ui .topbar { display: none }', }); await app.init(); cachedServer = configure({ app: expressApp, logSettings: { level: 'info' } }); console.log('NestJS application initialized successfully'); } return cachedServer; } export const handler = async ( event: APIGatewayProxyEvent, context: Context, ): Promise<APIGatewayProxyResult> => { console.log('Lambda handler invoked', { httpMethod: event.httpMethod, path: event.path, pathParameters: event.pathParameters, queryStringParameters: event.queryStringParameters, }); try { const server = await bootstrap(); const result = await server(event, context); console.log('Request processed successfully', { statusCode: result.statusCode, path: event.path, }); return result; } catch (error) { console.error('Error processing request:', error); return { statusCode: 500, headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type,Authorization,X-Requested-With', }, body: JSON.stringify({ error: 'Internal Server Error', message: 'An error occurred while processing the request', timestamp: new Date().toISOString(), }), }; } };
2. アプリケーションモジュール
src/app.module.ts
import { Module } from '@nestjs/common'; import { UsersModule } from './users/users.module'; import { ProductsModule } from './products/products.module'; import { OrdersModule } from './orders/orders.module'; import { HealthModule } from './health/health.module'; @Module({ imports: [ UsersModule, ProductsModule, OrdersModule, HealthModule, ], }) export class AppModule {}
API実装の詳細
1. ヘルスチェック API
システムの状態を確認するためのシンプルなAPIです。
src/health/health.controller.ts
import { Controller, Get } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; @ApiTags('health') @Controller('health') export class HealthController { @Get() @ApiOperation({ summary: 'ヘルスチェック' }) @ApiResponse({ status: 200, description: 'アプリケーションが正常に動作中' }) getHealth() { return { status: 'ok', timestamp: new Date().toISOString(), uptime: process.uptime(), environment: process.env.NODE_ENV || 'development', }; } @Get('ready') @ApiOperation({ summary: 'レディネスチェック' }) @ApiResponse({ status: 200, description: 'アプリケーションがリクエストを受け付け可能' }) getReady() { return { status: 'ready', timestamp: new Date().toISOString(), }; } }
2. ユーザー管理 API
CRUD操作を含む完全なユーザー管理機能を実装します。
DTOの定義
// src/users/dto/user.dto.ts import { IsEmail, IsNotEmpty, IsString, IsOptional, MinLength } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; export class CreateUserDto { @ApiProperty({ description: 'ユーザー名', example: 'john_doe' }) @IsNotEmpty() @IsString() username: string; @ApiProperty({ description: 'メールアドレス', example: 'john@example.com' }) @IsEmail() email: string; @ApiProperty({ description: 'パスワード', example: 'password123' }) @IsNotEmpty() @MinLength(6) password: string; @ApiProperty({ description: '氏名', example: 'John Doe', required: false }) @IsOptional() @IsString() fullName?: string; } export class UpdateUserDto { @ApiProperty({ description: 'ユーザー名', example: 'john_doe', required: false }) @IsOptional() @IsString() username?: string; @ApiProperty({ description: 'メールアドレス', example: 'john@example.com', required: false }) @IsOptional() @IsEmail() email?: string; @ApiProperty({ description: '氏名', example: 'John Doe', required: false }) @IsOptional() @IsString() fullName?: string; }
サービスの実装
// src/users/users.service.ts import { Injectable, NotFoundException, ConflictException } from '@nestjs/common'; import { CreateUserDto, UpdateUserDto, UserResponseDto } from './dto/user.dto'; interface User { id: string; username: string; email: string; password: string; fullName?: string; createdAt: Date; updatedAt: Date; } @Injectable() export class UsersService { private users: User[] = [ { id: '1', username: 'admin', email: 'admin@example.com', password: 'password123', fullName: 'Administrator', createdAt: new Date('2024-01-01'), updatedAt: new Date('2024-01-01'), }, ]; async findAll(): Promise<UserResponseDto[]> { return this.users.map(user => this.toResponseDto(user)); } async findOne(id: string): Promise<UserResponseDto> { const user = this.users.find(u => u.id === id); if (!user) { throw new NotFoundException(`ユーザーID ${id} が見つかりません`); } return this.toResponseDto(user); } async create(createUserDto: CreateUserDto): Promise<UserResponseDto> { // メールアドレスの重複チェック const existingUser = this.users.find(u => u.email === createUserDto.email); if (existingUser) { throw new ConflictException('このメールアドレスは既に使用されています'); } // ユーザー名の重複チェック const existingUsername = this.users.find(u => u.username === createUserDto.username); if (existingUsername) { throw new ConflictException('このユーザー名は既に使用されています'); } const newUser: User = { id: (this.users.length + 1).toString(), ...createUserDto, createdAt: new Date(), updatedAt: new Date(), }; this.users.push(newUser); return this.toResponseDto(newUser); } async update(id: string, updateUserDto: UpdateUserDto): Promise<UserResponseDto> { const userIndex = this.users.findIndex(u => u.id === id); if (userIndex === -1) { throw new NotFoundException(`ユーザーID ${id} が見つかりません`); } // 重複チェック(自分以外) if (updateUserDto.email) { const existingUser = this.users.find(u => u.email === updateUserDto.email && u.id !== id); if (existingUser) { throw new ConflictException('このメールアドレスは既に使用されています'); } } this.users[userIndex] = { ...this.users[userIndex], ...updateUserDto, updatedAt: new Date(), }; return this.toResponseDto(this.users[userIndex]); } async remove(id: string): Promise<void> { const userIndex = this.users.findIndex(u => u.id === id); if (userIndex === -1) { throw new NotFoundException(`ユーザーID ${id} が見つかりません`); } this.users.splice(userIndex, 1); } private toResponseDto(user: User): UserResponseDto { const { password, ...userResponse } = user; return userResponse; } }
AWS SAMによるデプロイ設定
1. SAMテンプレート
template.yaml
AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: NestJS API on AWS Lambda with API Gateway Globals: Function: Timeout: 30 MemorySize: 1024 Runtime: nodejs18.x Environment: Variables: NODE_ENV: production Api: Cors: AllowMethods: "'GET,POST,PUT,PATCH,DELETE,OPTIONS'" AllowHeaders: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'" AllowOrigin: "'*'" Resources: NestJSApiFunction: Type: AWS::Serverless::Function Properties: CodeUri: dist/ Handler: lambda.handler Description: NestJS API Lambda Function Events: ProxyApiRoot: Type: Api Properties: RestApiId: !Ref NestJSApi Path: / Method: ANY ProxyApiGreedy: Type: Api Properties: RestApiId: !Ref NestJSApi Path: /{proxy+} Method: ANY NestJSApi: Type: AWS::Serverless::Api Properties: StageName: Prod Description: NestJS API Gateway Cors: AllowMethods: "'GET,POST,PUT,PATCH,DELETE,OPTIONS'" AllowHeaders: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'" AllowOrigin: "'*'" BinaryMediaTypes: - '*/*' EndpointConfiguration: Type: REGIONAL Outputs: ApiGatewayUrl: Description: "API Gateway endpoint URL for Prod stage" Value: !Sub "https://${NestJSApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/" Export: Name: !Sub "${AWS::StackName}-ApiGatewayUrl" ApiDocumentationUrl: Description: "API Documentation URL (Swagger UI)" Value: !Sub "https://${NestJSApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/api" Export: Name: !Sub "${AWS::StackName}-ApiDocumentationUrl" HealthCheckUrl: Description: "Health Check endpoint URL" Value: !Sub "https://${NestJSApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/health" Export: Name: !Sub "${AWS::StackName}-HealthCheckUrl"
2. デプロイスクリプト
deploy.sh
#!/bin/bash set -e echo "🚀 Starting NestJS API deployment to AWS..." # TypeScriptビルド echo "Building NestJS application..." npm run build # package.jsonをdistフォルダにコピー echo "Copying package files to dist folder..." cp package.json dist/ cp package-lock.json dist/ # 本番用依存関係のインストール echo "Installing production dependencies..." cd dist npm install --production --omit=dev --silent cd .. # SAMビルド echo "Building SAM application..." sam build # デプロイ echo "Deploying to AWS..." sam deploy --resolve-s3 --no-confirm-changeset echo "Deployment completed successfully!"
実装時のポイントと注意事項
1. Lambda特有の考慮事項
コールドスタート対策
let cachedServer: any; async function bootstrap() { if (!cachedServer) { // サーバーインスタンスをキャッシュ cachedServer = configure({ app: expressApp }); } return cachedServer; }
インポート方法の注意
// ❌ 間違い import express from 'express'; import serverlessExpress from '@vendia/serverless-express'; // ✅ 正しい import * as express from 'express'; import { configure } from '@vendia/serverless-express';
2. CORS設定
API GatewayとNestJS両方でCORS設定が必要です。
NestJS側
app.enableCors({ origin: '*', methods: 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS', allowedHeaders: 'Content-Type,Authorization,X-Requested-With', });
SAMテンプレート側
Cors: AllowMethods: "'GET,POST,PUT,PATCH,DELETE,OPTIONS'" AllowHeaders: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'" AllowOrigin: "'*'"
3. エラーハンドリング
Lambda環境では適切なエラーハンドリングが重要です。
export const handler = async (event, context) => { try { const server = await bootstrap(); return await server(event, context); } catch (error) { console.error('Error processing request:', error); return { statusCode: 500, headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', }, body: JSON.stringify({ error: 'Internal Server Error', message: 'An error occurred while processing the request', timestamp: new Date().toISOString(), }), }; } };
デプロイと動作確認
1. デプロイ実行
# 実行権限を付与 chmod +x deploy.sh # デプロイ実行 ./deploy.sh
2. 動作確認
デプロイが完了すると、以下のURLが出力されます:
- API Gateway URL:
https://xxx.execute-api.ap-northeast-1.amazonaws.com/Prod/ - Swagger UI:
https://xxx.execute-api.ap-northeast-1.amazonaws.com/Prod/api - ヘルスチェック:
https://xxx.execute-api.ap-northeast-1.amazonaws.com/Prod/health
3. APIテスト例
# ヘルスチェック
curl https://your-api-gateway-url/health
# ユーザー作成
curl -X POST https://your-api-gateway-url/users \
-H "Content-Type: application/json" \
-d '{
"username": "testuser",
"email": "test@example.com",
"password": "password123",
"fullName": "Test User"
}'
# 商品一覧取得
curl https://your-api-gateway-url/products
# 注文作成
curl -X POST https://your-api-gateway-url/orders \
-H "Content-Type: application/json" \
-d '{
"userId": "1",
"items": [{"productId": "1", "quantity": 2}],
"shippingAddress": "東京都渋谷区...",
"notes": "午前中配送希望"
}'
まとめ
この記事では、NestJSを使用してAWS Lambda + API Gateway上で動作するサーバーレスAPIを構築する方法を詳しく解説しました。
学んだこと
NestJSの基本概念
- モジュール、コントローラー、サービス、DTOの役割
- デコレータベースの開発手法
AWS Lambda対応
- serverless-expressを使用したExpress統合
- コールドスタート対策
- 適切なインポート方法
API Gateway統合
- CORS設定の重要性
- プロキシ統合の設定方法
実践的な機能実装
- バリデーション機能
- エラーハンドリング
- Swagger UI統合
- 在庫管理を含むビジネスロジック
次のステップ
本記事で構築したAPIをさらに発展させるには:
データベース統合
- DynamoDB や RDS との連携
- TypeORM や Prisma の導入
認証・認可
- JWT トークン認証
- AWS Cognito 統合
テスト実装
- ユニットテスト
- 統合テスト
CI/CD パイプライン
- GitHub Actions
- AWS CodePipeline
監視・ログ
- CloudWatch メトリクス
- X-Ray トレーシング
NestJSとAWSの組み合わせにより、スケーラブルで保守性の高いサーバーレスAPIを効率的に構築できることがお分かりいただけたと思います。ぜひ実際に手を動かして試してみてください!
参考リンク
アプリケーションサービス本部
2024年中途入社
アプリケーションサービス本部にてAWSを活用したアプリケーション開発に携わっています!
趣味はお酒とバンドです