@chainsaws/lambda
v0.1.2
Published
TypeScript-friendly AWS Lambda handler utilities.
Maintainers
Readme
@chainsaws/lambda
TypeScript-friendly AWS Lambda handler utilities.
Installation
npm install @chainsaws/lambda
yarn add @chainsaws/lambda
pnpm add @chainsaws/lambdaThis package is ESM-only.
Quick Start
import { awsLambdaHandler, get_body } from "@chainsaws/lambda";
export const handler = awsLambdaHandler(async (event, context) => {
const body = get_body(event);
return {
message: "Success",
request_id: context.awsRequestId,
body,
};
});For API Gateway events, successful results are formatted as Lambda proxy responses automatically. For non-API-Gateway events, the wrapper returns the raw object result.
The package also exports aws_lambda_handler as a snake-case alias.
Handler Forms
Direct Form
export const handler = awsLambdaHandler(async (event, context, state) => {
state.requestId = context.awsRequestId;
return { ok: true, request_id: state.requestId };
});Curried Form
export const handler = awsLambdaHandler({
compactJson: true,
})(async () => {
return { ok: true };
});Use the direct form when you only need one handler. Use the curried form when you want to reuse one wrapper config across several handlers.
Lifecycle Hooks
The wrapper supports before, after, onError, and finally hooks. All
hooks are awaited inside the Lambda invocation before the wrapper returns.
Source: hook-lifecycle.mmd
import {
awsLambdaHandler,
shortCircuit,
} from "@chainsaws/lambda";
type HookState = {
startedAt?: number;
requestId?: string;
};
export const handler = awsLambdaHandler<
Record<string, unknown>,
{ ok: true; request_id: string; elapsed_ms: number },
{ awsRequestId?: string },
HookState
>({
createState: async (_event, context) => ({
requestId: context.awsRequestId ?? "unknown",
}),
hooks: {
before: async ({ state }) => {
state.startedAt = Date.now();
},
after: async ({ result, state }) => {
return {
...result,
elapsed_ms: Date.now() - (state.startedAt ?? Date.now()),
};
},
onError: async ({ phase, error }) => {
console.error("lambda hook error", phase, error);
},
finally: async ({ outcome, phase }) => {
console.log("lambda finished", outcome, phase);
},
},
})(async (_event, _context, state) => {
return {
ok: true,
request_id: state.requestId ?? "unknown",
elapsed_ms: 0,
};
});createState(...)
createState(...) builds the per-invocation state object that every hook and
the wrapped handler receive.
type HookState = {
requestId: string;
startedAt?: number;
};
const handler = awsLambdaHandler<
Record<string, unknown>,
{ ok: true },
{ awsRequestId?: string },
HookState
>({
createState: async (_event, context) => ({
requestId: context.awsRequestId ?? "unknown",
}),
hooks: {
before: async ({ state }) => {
state.startedAt = Date.now();
},
},
})(async (_event, _context, state) => {
console.log(state.requestId, state.startedAt);
return { ok: true };
});Use createState(...) for lightweight request-scoped metadata. Keep business
decisions such as authorization or validation in before hooks instead.
shortCircuit(...)
shortCircuit(...) returns a successful response from a before hook without
running the main handler.
import { awsLambdaHandler, shortCircuit } from "@chainsaws/lambda";
export const handler = awsLambdaHandler({
hooks: {
before: async ({ event }) => {
if (event.healthcheck === true) {
return shortCircuit({ ok: true, source: "healthcheck" });
}
},
},
})(async () => {
return { ok: false };
});This is different from throw:
shortCircuit(...)is a successful early return.shortCircuit(...)skips the main handler andafterhooks.shortCircuit(...)does not triggeronError.shortCircuit(...)still triggersfinally.
Policy Summary
beforeruns before the main handler and canthrowto stop execution.shortCircuit(...)returns early without running the main handler.afterruns only after a successful handler result and can replace it.onErrorruns whenbefore, the main handler, orafterthrows.finallyalways runs before the Lambda invocation returns.
Phase-Aware Error Observation
onError receives the phase where the failure happened. This is useful when
you want different reporting behavior for setup failures versus main-handler
failures.
import { HTTPException, awsLambdaHandler } from "@chainsaws/lambda";
export const handler = awsLambdaHandler({
hooks: {
before: async () => {
throw new HTTPException(401, "unauthorized");
},
onError: async ({ phase, error }) => {
console.error("lambda phase", phase, error);
},
finally: async ({ outcome, phase }) => {
console.log("lambda finished", outcome, phase);
},
},
})(async () => {
return { ok: true };
});Scheduler Integration
When you use @chainsaws/lambda together with @chainsaws/scheduler, the
recommended shape is:
- one frequent Lambda trigger, usually every minute
- inside the handler, gate real jobs with
runScheduledWorker(...)orrunScheduledWorkers([...]) - keep the actual job code in separate worker modules
Single Scheduled Worker
import { awsLambdaHandler } from "@chainsaws/lambda";
import { runScheduledWorker } from "@chainsaws/scheduler";
type EventBridgeEvent = Record<string, unknown>;
type LambdaContext = { awsRequestId?: string };
export const handler = awsLambdaHandler(
async (_event: EventBridgeEvent, context: LambdaContext) => {
await runScheduledWorker({
cron: "0 0 * * *",
tz: "Asia/Seoul",
worker: new URL("./jobs/delete-pending-users.js", import.meta.url),
payload: {
requestId: context.awsRequestId ?? null,
},
});
},
{
hooks: {
onError: async ({ phase, error }) => {
await botError.sendText(`scheduler lambda failed (${phase}): ${String(error)}`);
},
},
},
);Multiple Scheduled Workers
import { awsLambdaHandler } from "@chainsaws/lambda";
import { runScheduledWorkers } from "@chainsaws/scheduler";
export const handler = awsLambdaHandler(async (_event, context) => {
await runScheduledWorkers([
{
cron: "*/5 * * * *",
tz: "Asia/Seoul",
worker: new URL("./jobs/sync-metrics.js", import.meta.url),
payload: {
requestId: context.awsRequestId ?? null,
},
},
{
cron: "0 * * * *",
tz: "Asia/Seoul",
worker: new URL("./jobs/hourly-cleanup.js", import.meta.url),
payload: {
requestId: context.awsRequestId ?? null,
},
},
{
cron: "0 0 * * *",
tz: "Asia/Seoul",
worker: new URL("./jobs/delete-pending-users.js", import.meta.url),
payload: {
requestId: context.awsRequestId ?? null,
},
},
]);
});Worker Module Shape
Worker modules should use a default export and accept one serializable
payload.
// jobs/delete-pending-users.js
import { DynamoDBAPI } from "@chainsaws/dynamodb";
export default async function run(payload: {
requestId: string | null;
}) {
const db = new DynamoDBAPI("my-table", {
region: process.env.AWS_REGION ?? "ap-northeast-2",
});
// your real logic here
return {
deletedCount: 12,
requestId: payload.requestId,
};
}Practical Example Cases
1. Daily maintenance from a minutely EventBridge trigger
await runScheduledWorker({
cron: "0 0 * * *",
tz: "Asia/Seoul",
worker: new URL("./jobs/daily-maintenance.js", import.meta.url),
});2. Mix 5-minute, hourly, and daily jobs in one handler
await runScheduledWorkers([
{
cron: "*/5 * * * *",
worker: new URL("./jobs/five-minute-sync.js", import.meta.url),
},
{
cron: "0 * * * *",
worker: new URL("./jobs/hourly-report.js", import.meta.url),
},
{
cron: "0 0 * * *",
worker: new URL("./jobs/daily-rollup.js", import.meta.url),
},
]);3. Pass invocation context into the worker
await runScheduledWorker({
cron: "*/10 * * * *",
worker: new URL("./jobs/publish-heartbeat.js", import.meta.url),
payload: {
requestId: context.awsRequestId ?? null,
region: process.env.AWS_REGION ?? null,
stage: process.env.STAGE ?? null,
},
});4. Isolate CPU-heavy logic from the handler thread
await runScheduledWorker({
cron: "0 * * * *",
worker: new URL("./jobs/rebuild-search-index.js", import.meta.url),
payload: {
batchSize: 1000,
},
});For more details on the scheduler-side contract, see
@chainsaws/scheduler.
Package Surface
Most applications only need:
awsLambdaHandleraws_lambda_handlerHTTPExceptionAppErrorget_bodyget_headersget_source_ip
Lower-level helpers are also exported:
LambdaEventLambdaResponseget_event_dataHandlerConfigHandlerHooksBeforeHookAfterHookErrorHookFinallyHookContextshortCircuit
Error Handling
HTTPException
Throw HTTPException when you want to control the HTTP status code and optional
headers:
import { HTTPException, awsLambdaHandler } from "@chainsaws/lambda";
export const handler = awsLambdaHandler(async () => {
throw new HTTPException(
401,
{ message: "unauthorized" },
{ "WWW-Authenticate": "Bearer" },
);
});AppError
Throw AppError when you need both an HTTP status code and a stable
application code:
import { AppError, awsLambdaHandler } from "@chainsaws/lambda";
export const handler = awsLambdaHandler(async () => {
throw new AppError("USER_NOT_FOUND", "user missing", 404);
});onError
Use onError to notify Slack, Sentry, or a custom alerting sink:
export const handler = awsLambdaHandler({
hooks: {
onError: async ({ phase, event, error, state }) => {
console.error("lambda failure", {
phase,
event,
error,
state,
});
},
},
})(async () => {
throw new Error("unexpected failure");
});onError is phase-aware, receives the shared state, and runs before the
wrapper formats the final error response.
Event Helpers
get_body
Parses API Gateway event.body as JSON and returns only object payloads.
const body = get_body(event);get_headers
Returns normalized event headers.
const headers = get_headers(event);get_source_ip
Extracts requestContext.identity.sourceIp when present.
const sourceIp = get_source_ip(event);LambdaEvent
For structured event access:
import { LambdaEvent } from "@chainsaws/lambda";
const lambdaEvent = LambdaEvent.from_dict(event);
const parsed = lambdaEvent.get_json_body();
const isApiGateway = LambdaEvent.is_api_gateway_event(event);
const isAlb = LambdaEvent.is_alb_event(event);Response Formatting
LambdaResponse.create(...) builds Lambda proxy responses and passes through
already formatted responses unchanged.
import { LambdaResponse } from "@chainsaws/lambda";
return LambdaResponse.create(
{ ok: true },
{
status_code: 200,
content_type: "application/json",
},
);For API Gateway events, handler results are wrapped as:
{
"data": {
"ok": true
}
}Package Scope
This package is intentionally focused on Lambda entrypoint wrapping and API Gateway response formatting. It does not include higher-level routing or resolver abstractions such as:
- API Gateway resolver classes
- ALB resolver classes
- WebSocket resolver classes
- OpenAPI helpers
- dependency injection helpers
