#概要
普段、API Gateway + Lambda でAPIを作成する時は、特にフレームワーク等は使用せず一から作成していたのですが、今回機会があり Nest.js を利用したので備忘録として残しておきます。
Next.jsとはなのですが、
・TypeScriptで書けるNode.jsフレームワーク
・GraphQL、TypeORMなどもサポートしている
・基本的な構成が用意されていたり、デコレータを利用したりと、効果的かつ安易に作成することができる
みたいな感じとのことです。
公式サイト
https://docs.nestjs.com/
#前提
・AWSを利用できる環境
・基本的なTypeScriptの知識
##NestJSインストール
NestJSはnpmパッケージとして提供されています
npm install @nestjs/cli -g
インストールしたCLIを使って、プロジェクトの作成を行います。
nest new my-app
インストレーションに従ったあと、プリケーションサーバーとして、以下コマンドで起動してみます。
npm run start
http://localhost:3000/ にアクセスすると、Hello worldが表示されます。
##NestJSの構成
プロジェクトの作成をすると、srcフォルダ配下に以下の4つのファイルが生成されます。
NestJSではこれらの構成を利用して開発をすることが可能です。
###main.ts
nestjsのエントリーポイントとなり、ここでサーバのインスタンスを作成し、listenします。
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();
app.controller.ts
ルーティングを定義します。
TypeScriptのデコレ―タを利用して、機能を簡単に実装することができます。
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}
###app.service.ts
各種機能をControllerに提供します。
Nest.jsではprovidersにあたります。
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}
###app.module.ts
controllerやproviderの依存関係を定義します。1つ以上のapp.module.tsが必要となっています。
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
#実装
それでは実際に、簡単なアプリケーションをAWS上に構築してみます。
■ 仕様
[POST] /users :ユーザ情報追加
[GET] /users :ユーザ情報取得
※ リクエスト時は、x-api-keyをヘッダーに付与。(x-api-keyはコード内部に埋め込み)
##インフラ構築
インフラ環境は、Serverless Frameworkを利用します。
以下のコードで、API Gateway + Lambda + DynamoDBが一撃で作成されます。
DynamoDBはプライマリパーティションキーをnameとしています。
(lambdaに展開するコードを格納するS3バケットはあらかじめ手動で作成しました。ここでは、layersDeploymentBucket: test-api-service-lambda-layers になります。)
service: test-api-service
provider:
name: aws
runtime: nodejs12.x
region: ap-northeast-1
environment:
USER_TABLE_NAME: 'test-api-table'
iamRoleStatements:
- Effect: Allow
Action:
- dynamodb:*
Resource: 'arn:aws:dynamodb:*:*:table/test-api-table'
plugins:
- serverless-layers
custom:
serverless-layers:
layersDeploymentBucket: test-api-service-lambda-layers
package:
individually: true
include:
- dist/**
exclude:
- '**'
functions:
index:
handler: dist/index.handler
events:
- http:
cors: true
path: '/'
method: any
- http:
cors: true
path: '{proxy+}'
method: any
resources:
Resources:
DynamoDbTable:
Type: 'AWS::DynamoDB::Table'
Properties:
AttributeDefinitions:
- AttributeName: name
AttributeType: S
KeySchema:
- AttributeName: name
KeyType: HASH
ProvisionedThroughput:
ReadCapacityUnits: 1
WriteCapacityUnits: 1
TableName: 'test-api-table'
##アプリケーション構築
API Gateway + Lambda環境でNest.jsを動作させるため、src配下にindex.tsを作成します。
import { APIGatewayProxyHandler } from 'aws-lambda';
import { NestFactory } from '@nestjs/core';
import { Server } from 'http';
import { ExpressAdapter } from '@nestjs/platform-express';
import * as serverless from 'aws-serverless-express';
import * as express from 'express';
import { AppModule } from './app.module';
let cachedServer: Server;
const bootstrapServer = async (): Promise<Server> => {
const expressApp = express();
const adapter = new ExpressAdapter(expressApp);
const app = await NestFactory.create(AppModule, adapter);
app.enableCors();
app.init();
return serverless.createServer(expressApp);
};
export const handler: APIGatewayProxyHandler = async (event, context) => {
if (!cachedServer) {
cachedServer = await bootstrapServer();
}
const result = await serverless.proxy(cachedServer, event, context, 'PROMISE')
.promise;
return result;
};
app.service.ts と app.controller.ts を以下の様に修正します。
@Post('/users') と @Get('/users')を指定して、2つのAPIを実装しています。
import { Injectable } from '@nestjs/common';
import { AppRepository } from './app.repository';
@Injectable()
export class AppService {
async createUser(body): Promise<string> {
if (!body || !body.name) {
return JSON.stringify({ error: 'name は必須項目です' });
}
const result = await new AppRepository().create(body);
return JSON.stringify(result.Attributes);
}
async getUsers(): Promise<string> {
const result = await new AppRepository().getAll();
return JSON.stringify(result.Items);
}
}
import { Controller, Get, Post, Delete, Req, Body } from '@nestjs/common';
import { AppService } from './app.service';
import { Request } from 'express';
const API_KEY = '*****';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Post('/users')
async createUser(@Body() body, @Req() request: Request): Promise<string> {
this.checkApiKey(request);
return await this.appService.createUser(body);
}
@Get('/users')
async getUsers(@Req() request: Request): Promise<string> {
this.checkApiKey(request);
return await this.appService.getUsers();
}
private checkApiKey(request: Request): void {
if ((request.headers['x-api-key'] as string) !== API_KEY) {
throw new Error('API KEY ERROR');
}
}
}
DynamoDB操作用に app.repository.ts を作成します。
import * as AWS from 'aws-sdk';
import { InternalServerErrorException } from '@nestjs/common';
export class AppRepository {
private tableName = process.env.USER_TABLE_NAME;
async create(user): Promise<AWS.DynamoDB.DocumentClient.UpdateItemOutput> {
let updateExpression = 'set ';
const expressionAttributeValues = {} as any;
const expressionAttributeNames = {} as any;
for (const [key, value] of Object.entries(user)) {
if (!(key === 'name')) {
updateExpression += `#${key} = :${key}, `;
expressionAttributeValues[`:${key}`] = value;
expressionAttributeNames[`#${key}`] = `${key}`;
}
}
updateExpression = updateExpression.slice(0, -2);
try {
return await new AWS.DynamoDB.DocumentClient()
.update({
TableName: this.tableName,
Key: {
name: user.name,
},
UpdateExpression: updateExpression,
ExpressionAttributeValues: expressionAttributeValues,
ExpressionAttributeNames: expressionAttributeNames,
ReturnValues: 'ALL_NEW',
})
.promise();
} catch (error) {
throw new InternalServerErrorException(error);
}
}
async getAll(): Promise<AWS.DynamoDB.DocumentClient.ScanOutput> {
return await new AWS.DynamoDB.DocumentClient()
.scan({
TableName: this.tableName,
})
.promise();
}
}
##デプロイ
それでは、AWS環境にデプロイしてみます。
$ serverless deploy
##動作確認
以下のcurlコマンドで意図した通りにデータを登録・取得できることが確認できました。
[POST] /users :ユーザ情報追加
curl --request POST \
--url https://{API_GATEWAY_ENDPOINT}/dev/users \
--header 'Content-Type: application/json' \
--header 'x-api-key: *****' \
--data '{
"name": "test",
"ID": "12345"
}'
> {"ID":"12345","name":"test"}
[GET] /users :ユーザ情報取得
curl --request GET \
--url https://{API_GATEWAY_ENDPOINT}/dev/users \
--header 'x-api-key: *****'
> [{"ID":"12345","name":"test"}]
#まとめ
TypeScriptで書けるNode.jsフレームワークのNest.jsを利用して、簡単なAPIをAWS上に構築してみました。
普段API Gateway + Lambdaを利用するときは、ライブラリ等利用せず、一からAPI作成することが多かったのですが、
今回シンプルかつ読みやすいコードで実装できたので、こういったライブラリを利用することも良いかなと感じました。
単体テストはまだかけていないので、次はそちらを触ってみようと思います。