メインコンテンツまでスキップ

WebAPI 実装ガイド

本カタログで WebAPI を実装するプラクティスを記載したドキュメントです。ポイントは以下2点です。

  • WebAPI の主要機能は AWS 公式ライブラリ Powertools for AWS Lambda (Python) を利用
  • 内部アーキテクチャとして Lambdalith 構成を採用
    • API ごとに Lambda リソースを分離するより、パフォーマンスとと運用便宜性に優れる
    • 従来の WebAPI フレームワークの開発構成を踏襲できる

ユーザー情報を登録する API の実装

ユーザー情報の入力を受け、ユーザーデータを新規登録する API を実装します。

API 仕様

以下のような仕様のサマリーを作成します。

  • 正常系

    [Request sample]
    POST /users
    {
    "name": "Shigeo",
    "country": "Japan",
    "age": 30
    }

    [Response sample 1]
    200 OK
    {
    "name": "Shigeo",
    "country": "Japan",
    "age": 30
    }
  • 異常系(name未指定)

    [Request sample]
    POST /users
    {
    # "name": "Shigeo",
    "country": "Japan",
    "age": 30
    }

    [Response sample]
    400 Bad Request
    {
    "message": "Missing name. name is required query parameter."
    }

大まかな処理の流れ

Lambdalith の推奨構成として、大きく以下4つのレイヤーで構成され、上から順に呼ばれていきます。

レイヤー名サマリー
API Routingマッピングされた Action を呼び出す
Actionいわゆる Controller 層で、入力チェック、API本体のロジック(Serviceレイヤー)の呼び出しを行う
ServiceAPI のメインロジック・ビジネスロジック
Daoデータベースアクセス処理・クエリー実行

他にも、AWS SDK 実装を分離した aws/、外部ライブラリを分離した lib/、データのモデリングやスキーマを定義する models/ など、必要に応じて適宜ディレクトリを構成してください。

API Routing 実装

リクエストを受け付けると、Lambda 関数のエントリーポイント( src/sample_api/index.ts )にリクエスト情報が入った event オブジェクトが渡されます。Powertools から提供されるデコレータを利用して以下のように適切な Action に振り分けます。

また、Lambdalith 構成での API ルーティングライブラリとしては Hono を利用しています。他にも、lambda-api を利用するなど選択肢があります。

実装例

  • src/sample-api/SampleAPI.ts
import * as Actions from '~/sample-api/actions'
import { Hono } from 'hono'

export class SampleAPI {
public app: Hono
public v1: Hono<HonoEnv>

public constructor() {
this.app = new Hono()
this.v1 = new Hono<HonoEnv>()
}

public setV1Routes() {
/* User routes */
this.v1.post('/v1/users', async (ctx) => await new Actions.CreateUserAction().handle(ctx))
this.v1.get('/v1/users/:user_id', async (ctx) => await new Actions.GetUserAction().handle(ctx))

this.app.route('/v1', this.v1)

return this
}
}

Action 実装

「Action」は、以下の役割を担当します。

  • API 処理の「入り口」と「出口」となる部分
  • 入力チェック、Service 実行、レスポンスデータ型の整理等を行う
  • Controller 層と言われることもある

実装例

  • src/sample-api/actions/CreateUserAction.ts
import type { Context, TypedResponse } from 'hono'
import type { HonoEnv } from '~/sample-api/libs'
import { logger } from '~/sample-api/logger'
import type { CreateUserPayload, CreateUserResponse } from '~/sample-api/schemas'
import { UserService } from '~/sample-api/services'

export class CreateUserAction {
public async handle(ctx: Context<HonoEnv>): Promise<TypedResponse<CreateUserResponse>> {
logger.info('CreateUserAction initiated')

const userPayload = await ctx.req.json<CreateUserPayload>()

const service = new UserService()
const newUser = await service.createUser(userPayload)
logger.info('User created successfully', { user: newUser })

return ctx.json({
id: newUser.id,
name: newUser.name,
age: newUser.age || undefined,
country: newUser.country || undefined,
})
}
}

Service 実装

「Service」は、API のメインロジック、ビジネスロジックを担当する部分です。

実装例

  • src/sample-api/services/UserService.ts

export class UserService {
private userDao: UserDao
private dynamoDb: DynamoDB

public constructor() {
this.userDao = new UserDao()
this.dynamoDb = new DynamoDB()
}

public async createUser(payload: CreateUserPayload) {
// ユーザー作成のための処理を実装します
// 例えば、ユニークな ID 採番ルールがある場合は、採番処理を実装します
// 登録準備が整ったら、DAO を経由してデータベースにユーザー情報の書き込み処理を実施します
const resultFromRDB = await this.createUserInRDB(payload)
logger.info('Result from RDB', { data: resultFromRDB })

const resultFromDynamoDB = await this.createUserInDynamoDB(payload)
logger.info('Result from DynamoDB', { data: resultFromDynamoDB })

// Returns result from RDB here for sample purpose
return resultFromRDB
}

public async getUserFromRdb(userId: number) {
const result = await this.userDao.findById(userId)

if (!result) {
throw new SampleException({
status: StatusCode.NOT_FOUND,
errorCode: 'USER_NOT_FOUND',
message: 'User does not exist in RDB',
})
}
return result
}
}

DAO 実装

「DAO」は、データベースにアクセスし、クエリーを実行する処理を担当します。

実装例

  • src/sample-api/daos/UserDao.ts
import { prisma, type User } from '~/sample-api/libs'
import { logger } from '~/sample-api/logger'
import type { CreateUserPayload } from '~/sample-api/schemas'

export class UserDao {
async findById(id: number): Promise<User | null> {
logger.info('Finding user by ID...', { id })
const user = await prisma.user.findUnique({
where: { id },
})
logger.info('Retrieved user', { id })
return user
}

async create(payload: CreateUserPayload): Promise<User> {
logger.info('Creating new user...', { payload })
const { name, country, age } = payload
const user = await prisma.user.create({
data: {
name,
country,
age,
},
})
logger.info(`Created new user with auto-incremented id: ${user.id}`)
return user
}
}

動作確認

ローカル実行によるデバッグとユニットテスト」を参照してください。