Skip to main content

Other Recommended Implementation Patterns

This section introduces recommended implementation patterns for running on AWS Lambda, regardless of application type.

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.

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.