npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@chainsaws/lambda

v0.1.2

Published

TypeScript-friendly AWS Lambda handler utilities.

Readme

@chainsaws/lambda

TypeScript-friendly AWS Lambda handler utilities.

Installation

npm install @chainsaws/lambda
yarn add @chainsaws/lambda
pnpm add @chainsaws/lambda

This 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.

Hook execution order

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 and after hooks.
  • shortCircuit(...) does not trigger onError.
  • shortCircuit(...) still triggers finally.

Policy Summary

  • before runs before the main handler and can throw to stop execution.
  • shortCircuit(...) returns early without running the main handler.
  • after runs only after a successful handler result and can replace it.
  • onError runs when before, the main handler, or after throws.
  • finally always 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(...) or runScheduledWorkers([...])
  • 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:

  • awsLambdaHandler
  • aws_lambda_handler
  • HTTPException
  • AppError
  • get_body
  • get_headers
  • get_source_ip

Lower-level helpers are also exported:

  • LambdaEvent
  • LambdaResponse
  • get_event_data
  • HandlerConfig
  • HandlerHooks
  • BeforeHook
  • AfterHook
  • ErrorHook
  • FinallyHook
  • Context
  • shortCircuit

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