WebAPI Implementation Guide
This document describes the practices for implementing WebAPI in this catalog. The key points are as follows:
- WebAPI's main features utilize the AWS official library Powertools for AWS Lambda (Python)
- Adopts a Lambdalith architecture internally
- Superior performance and operational convenience compared to separating Lambda resources for each API
- Can follow the development structure of traditional WebAPI frameworks
Implementing an API to Register User Information
Implement an API that receives user information input and registers new user data.
API Specification
Create a specification summary as follows:
-
Normal case
[Request sample]
POST /users
{
"name": "Shigeo",
"country": "Japan",
"age": 30
}
[Response sample 1]
200 OK
{
"name": "Shigeo",
"country": "Japan",
"age": 30
} -
Error case (name not specified)
[Request sample]
POST /users
{
# "name": "Shigeo",
"country": "Japan",
"age": 30
}
[Response sample]
400 Bad Request
{
"message": "Missing name. name is required query parameter."
}
Overall Processing Flow
As a recommended configuration for Lambdalith, it consists of the following four major layers that are called in order from top to bottom:
Layer Name | Summary |
---|---|
API Routing | Calls the mapped Action |
Action | Controller layer that performs input validation and calls the API main logic (Service layer) |
Service | API main logic and business logic |
Dao | Database access processing and query execution |
Additionally, please configure directories as needed, such as aws/
for separating AWS SDK implementations, lib/
for separating external libraries, and models/
for defining data modeling and schemas.
API Routing Implementation
When a request is received, an event
object containing request information is passed to the Lambda function's entry point (src/sample_api/index.ts
). Using decorators provided by Powertools, it is routed to the appropriate Action as follows:
For API routing library in Lambdalith configuration, we use Hono. There are other options such as using lambda-api.
Implementation Example
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 Implementation
"Action" is responsible for the following roles:
- Entry and exit points for API processing
- Performs input validation, Service execution, and response data type organization
- Sometimes called the Controller layer
Implementation Example
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 Implementation
"Service" is the part responsible for the main logic and business logic of the API.
Implementation Example
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) {
// Implement processing for user creation
// For example, if there are unique ID numbering rules, implement the numbering process
// When registration is ready, perform user information writing to the database via 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 Implementation
"DAO" is responsible for accessing the database and executing queries.
Implementation Example
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
}
}
Testing
Please refer to "Local Debugging and Unit Testing".