Other Recommended Implementation Patterns
This section introduces recommended implementation patterns for running on AWS Lambda, regardless of application type.
Recommended Configuration When Using AWS SDK
Implement and place AWS SDK wrapper classes in the aws
directory and use them as modules.
The benefits are as follows:
- Improved reusability
- Serverless applications frequently call AWS services
- Improved testability and maintainability
- Easy to mock
- Minimize impact scope from AWS SDK version upgrades or specification changes
- Improved code readability
Not only for AWS SDK, but also when calling external APIs or using libraries, you can enjoy the above benefits by using a similar configuration.
Wrapper Class Example
For example, the following is an implementation example of a wrapper class for accessing S3.
import {
ListObjectsV2Command,
type ListObjectsV2CommandInput,
PutObjectCommand,
type PutObjectCommandInput,
S3Client,
} from '@aws-sdk/client-s3'
const client = new S3Client({ region: 'us-east-1' })
export class S3 {
private s3Client: S3Client
public constructor() {
this.s3Client = new S3Client({ region: 'us-east-1' })
}
public async putObject(bucket: string, key: string, body: any, metadata = {}) {
const payload = {
Bucket: bucket,
Key: key,
Body: body,
} as PutObjectCommandInput
if (Object.keys(metadata).length) {
payload.Metadata = metadata
}
const command = new PutObjectCommand(payload)
const uploadRes = await this.s3Client.send(command)
return uploadRes
}
public async getObjectAsString(bucket: string, fileKey: string) {
const command = new GetObjectCommand({
Bucket: bucket,
Key: fileKey,
})
const object = await client.send(command)
return await object.Body?.transformToString()
}
public async listObjects(bucket: string, prefix: string, continuationToken = '') {
const input = {
Bucket: bucket,
Prefix: prefix,
} as ListObjectsV2CommandInput
if (continuationToken) {
input.ContinuationToken = continuationToken
}
const command = new ListObjectsV2Command(input)
const response = await this.s3Client.send(command)
return response
}
}
API Error Handling
While the method may vary depending on the API routing library you use, the policy is to implement and use error handlers and custom error classes. In this catalog, we create custom errors as follows.
src/sample-api/errors/SampleException.ts
import { HTTPException } from 'hono/http-exception'
import type { StatusCode } from '~/sample-api/exception'
export class SampleException extends HTTPException {
public errorCode: string
public statusCode: number
public constructor(params: { status: StatusCode; errorCode: string; message: string; cause?: Error }) {
super(params.status, { message: params.message })
super.cause = params.cause
this.errorCode = params.errorCode
Error.captureStackTrace(this, this.constructor)
}
}
When you want to return an error response within API logic, throw an exception (error) as follows.
src/sample-api/services/UserService.ts
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
}
The exception thrown here is caught by the following ExceptionHandler
and returned as an API error response.
src/sample-api/errors/ExceptionHandler.ts
import { SampleException, StatusCode } from '~/sample-api/exception'
import { logger } from '~/sample-api/logger'
export const handleException = (err: Error): Response => {
if (err instanceof SampleException) {
const body = JSON.stringify({
code: err.errorCode,
message: err.message,
})
return new Response(body, {
status: err.status,
headers: {
'content-type': 'application/json; charset=UTF-8',
},
})
}
logger.error('## error thrown in handleException', { err })
const body = JSON.stringify({
code: 'UNEXPECTED_ERROR',
message: err.message,
})
return new Response(body, {
status: StatusCode.INTERNAL_SERVER_ERROR,
headers: {
'content-type': 'application/json; charset=UTF-8',
},
})
}
Logger
This section explains how to output logs using the Logger from Powertools for AWS Lambda (TypeScript) to output the JSON structured logs recommended in this catalog.
- Powertools for AWS Lambda (TypeScript) - Logger
Usage
Instantiate and use it as follows. You can also add or remove items as needed. Please define the logger
class separately at the Lambda entry point start or initialize and configure it (reflecting the context).
src/sample-api/logger/Logger.ts
src/sample-event/logger/Logger.ts
import { Logger } from '@aws-lambda-powertools/logger'
const logger = new Logger()
export { logger }
src/sample-event/index.ts
import { logger } from '~/sample-event/logger'
export const handler = async (event: S3Event, context: Context): Promise<void> => {
logger.addContext(context)
// ...
}
By default, logs are output as follows.
{
"level": "INFO",
"location": "create_news:19",
"message": "Created new news record - {'news_type': 'sports', 'title': 'sample_news_title', 'content': 'sample_news_conten'}",
"timestamp": "2025-04-20 01:51:45,781+0000",
"service": "service_undefined",
"cold_start": true,
"function_name": "tester-function",
"function_memory_size": 256,
"function_arn": "invoked_function_arn",
"function_request_id": "aws_request_id"
}
Log Search in CloudWatch Logs
Based on the above format, you can perform log searches in CloudWatch Logs as follows.
# Search by specifying log level
{ $.level = "ERROR" }
# Search by specifying Request ID
{ $.requestId = "xxx" }
When searching in CloudWatch Logs, you will get results like the following.