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レイヤー)の呼び出しを行う |
Service | API のメインロジック・ビジネスロジック |
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
}
}
動作確認
「ローカル実行によるデバッグとユニットテスト」を参照してください。