9
12

More than 3 years have passed since last update.

[Nest.js] API Gateway + Lambda + DynamoDB 構成を実現してみた

Last updated at Posted at 2021-03-17

概要

普段、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します。

main.ts
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のデコレ―タを利用して、機能を簡単に実装することができます。

app.controller.ts
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にあたります。

app.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class AppService {
  getHello(): string {
    return 'Hello World!';
  }
}

app.module.ts

controllerやproviderの依存関係を定義します。1つ以上のapp.module.tsが必要となっています。

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 になります。)

serverless.yml
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を作成します。

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を実装しています。

app.service.ts
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);
  }
}

app.controller.ts
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 を作成します。

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作成することが多かったのですが、
今回シンプルかつ読みやすいコードで実装できたので、こういったライブラリを利用することも良いかなと感じました。
単体テストはまだかけていないので、次はそちらを触ってみようと思います。

9
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
12