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

@vyriy/handler

v0.9.3

Published

Composable AWS Lambda handler chains and wrappers for Vyriy projects

Downloads

6,067

Readme

@vyriy/handler

Part of Vyriy - a calm architecture toolkit for TypeScript, React, SSR, SSG, APIs, and cloud-ready apps.

Full documentation: https://vyriy.dev/docs/handler/

Composable AWS Lambda handler chains and wrappers for Vyriy projects.

Purpose

@vyriy/handler provides ready-made handler chains for common AWS Lambda workloads and small reusable wrappers for logging, timeouts, smoke checks, development chaos injection, context setup, headers, CORS, healthchecks, and error handling.

It is designed for projects that want a consistent handler pipeline without repeating the same boilerplate in every Lambda entrypoint.

Install

With npm:

npm install @vyriy/handler

With Yarn:

yarn add @vyriy/handler

For TypeScript Lambda projects, install AWS Lambda types as a development dependency:

yarn add @types/aws-lambda

The awslambda response streaming helper is a global provided by the AWS Lambda Node.js runtime. It is not imported from aws-lambda; @types/aws-lambda only lets TypeScript type-check that global.

API

Use api for API Gateway-style Lambda handlers:

import { api } from '@vyriy/handler';

export const handler = api(async (event) => ({
  statusCode: 200,
  body: JSON.stringify({
    path: event.path,
  }),
}));

api includes error handling, logging, timeout handling, context setup, smoke checks, healthcheck handling, default headers, CORS preflight handling, and development chaos injection.

Use create.api(...) when a project needs a configured API chain. Passing headers replaces the default API headers:

import { create } from '@vyriy/handler';

const api = create.api({
  headers: {
    'access-control-allow-origin': '*',
    'content-type': 'application/json',
  },
  healthcheck: {
    path: '/ready',
  },
});

export const handler = api(async () => ({
  statusCode: 200,
  body: JSON.stringify({ ok: true }),
}));

The API-specific entrypoint exposes the same chain as api:

import { api, create } from '@vyriy/handler/api';

Event Sources

The event-source chains share the Lambda wrapper set: withError, withLogger, withTimeout, withContext, and withSmoke. Failures are rethrown so AWS event-source retry behavior still works.

S3

import { s3 } from '@vyriy/handler';

export const handler = s3(async (event) => {
  for (const record of event.Records) {
    console.info(record.s3.bucket.name, record.s3.object.key);
  }
});

SQS

import { sqs } from '@vyriy/handler';

export const handler = sqs(async (event) => {
  for (const record of event.Records) {
    console.info(record.body);
  }
});

SNS

import { sns } from '@vyriy/handler';

export const handler = sns(async (event) => {
  for (const record of event.Records) {
    console.info(record.Sns.Message);
  }
});

DynamoDB Streams

import { dynamodb } from '@vyriy/handler';

export const handler = dynamodb(async (event) => {
  for (const record of event.Records) {
    console.info(record.eventName);
  }
});

EventBridge

import { eventBridge } from '@vyriy/handler';

export const handler = eventBridge(async (event) => {
  console.info('EventBridge event:', event.source, event['detail-type'], event.detail);
});

Schedule

import { schedule } from '@vyriy/handler';

export const handler = schedule(async (event) => {
  console.info('Scheduled event:', event['detail-type']);
});

SES

import { ses } from '@vyriy/handler';

export const handler = ses(async (event) => {
  for (const record of event.Records) {
    console.info(record.ses.mail.messageId, record.ses.mail.source);
  }
});

ses targets SES receipt rule Lambda events for incoming email. SES event publishing notifications can still be handled through the sns chain when delivered via SNS.

HTTP

Use httpApi for native Node HTTP handlers. Handlers receive (request, response) and own the response lifecycle, which fits transports such as MCP Streamable HTTP:

import { httpApi } from '@vyriy/handler';

export const handler = httpApi(async (request, response) => {
  response
    .writeHead(200, {
      'content-type': 'application/json',
    })
    .end(JSON.stringify({ ok: true, url: request.url }));
});

httpApi includes error handling, request logging, healthcheck handling, default headers, and CORS preflight handling. It sets no default content-type because native handlers own the response body format.

Use the runtime-specific entrypoint when that reads better:

import { api, create } from '@vyriy/handler/api/http';

Configure a native HTTP chain with create.httpApi(...). Passing headers replaces the default HTTP headers:

import { create } from '@vyriy/handler';

const httpApi = create.httpApi({
  headers: {
    'access-control-allow-headers': 'content-type, mcp-protocol-version',
    'access-control-allow-methods': 'GET, POST, OPTIONS',
    'access-control-allow-origin': '*',
    'x-content-type-options': 'nosniff',
  },
});

export const handler = httpApi(async (_request, response) => {
  response.writeHead(200).end('ok');
});

Run it locally or in a container with httpServer from @vyriy/server:

import { httpServer } from '@vyriy/server';

import { handler } from './handler.js';

httpServer(handler);

Build a custom native HTTP pipeline with httpCompose(...) and httpFactory(...):

import { httpCompose, httpFactory, httpWithError } from '@vyriy/handler';

const httpWithRequestId = httpFactory<{ headerName?: string }>(async (handler, args, options = {}) => {
  const [request] = args;
  const requestId = request.headers[options.headerName ?? 'x-request-id'];

  if (requestId) {
    console.info('Request ID:', requestId);
  }

  await handler(...args);
});

export const handler = httpCompose(
  httpWithError(),
  httpWithRequestId(),
)(async (_request, response) => {
  response.writeHead(200).end('ok');
});

The lower-level helpers are also available from runtime subpaths:

import { compose } from '@vyriy/handler/api/http/compose';
import { factory } from '@vyriy/handler/api/http/factory';

Stream

Use streamApi for Lambda response streaming. Stream handlers receive (event, responseStream, context) and write directly to the response stream:

import { streamApi } from '@vyriy/handler';

export const handler = streamApi(async (event, responseStream) => {
  responseStream.setContentType?.('text/plain');
  responseStream.write(`Request path: ${event.path}\n`);
  responseStream.write('Part 1 of the response...');
  responseStream.end('Part 2 of the response...');
});

Use the same handler in AWS Lambda response streaming:

import { handler } from './handler.js';

export const main = awslambda.streamifyResponse(handler);

Run the same handler locally, in Docker, or in a Fargate-style HTTP runtime:

import { streamServer } from '@vyriy/server';

import { handler } from './handler.js';

streamServer(handler);

You can also inline the same shape in one file:

import { streamApi } from '@vyriy/handler';

export const main = awslambda.streamifyResponse(
  streamApi(async (event, responseStream) => {
    responseStream.setContentType?.('text/plain');
    responseStream.write(`Request path: ${event.path}\n`);
    responseStream.end('ok');
  }),
);

The runtime-specific entrypoint exposes the same chain as api:

import { api, create } from '@vyriy/handler/api/stream';

streamApi includes the same wrapper behavior as api, adapted for response streaming. Passing headers to create.streamApi(...) replaces the default stream headers.

Custom stream pipelines use stream-specific compose and factory helpers:

import { streamCompose, streamFactory, streamWithApiError } from '@vyriy/handler';

const streamWithRequestId = streamFactory<{ headerName?: string }>(async (handler, args, options = {}) => {
  const [event] = args;
  const requestId = event.headers?.[options.headerName ?? 'x-request-id'];

  if (requestId) {
    console.info('Request ID:', requestId);
  }

  await handler(...args);
});

export const handler = streamCompose(
  streamWithApiError(),
  streamWithRequestId(),
)(async (_event, responseStream) => {
  responseStream.end('ok');
});

The lower-level helpers are also available from runtime subpaths:

import { compose } from '@vyriy/handler/api/stream/compose';
import { factory } from '@vyriy/handler/api/stream/factory';

Create

create builds configured versions of the prebuilt chains while keeping the standard exports unchanged:

import { create } from '@vyriy/handler';

const { api, httpApi, s3, sqs, streamApi } = create;

Each key is a factory:

import { create } from '@vyriy/handler';

const httpApi = create.httpApi({
  healthcheck: {
    path: '/health',
  },
  headers: {
    'access-control-allow-origin': '*',
    'access-control-allow-methods': 'GET, POST, OPTIONS',
    'access-control-allow-headers': 'content-type, mcp-protocol-version',
    'x-content-type-options': 'nosniff',
  },
});

export const handler = httpApi(async (_request, response) => {
  response.writeHead(200).end('ok');
});

Available factories:

  • create.api(options?)
  • create.httpApi(options?)
  • create.streamApi(options?)
  • create.dynamodb(options?)
  • create.eventBridge(options?)
  • create.s3(options?)
  • create.schedule(options?)
  • create.ses(options?)
  • create.sns(options?)
  • create.sqs(options?)

For api, httpApi, and streamApi, passing headers replaces the default header set for that configured chain.

Runtime subpaths also expose local factories:

import { create as createHttpApi } from '@vyriy/handler/api/http';
import { create as createStreamApi } from '@vyriy/handler/api/stream';

const httpApi = createHttpApi({
  headers: {
    'access-control-allow-origin': '*',
  },
});

const streamApi = createStreamApi();

Custom Wrappers

Use compose(...) to build a custom Lambda pipeline from decorators:

import { compose, withChaos, withContext, withError, withLogger, withTimeout } from '@vyriy/handler';

export const handler = compose(
  withError(),
  withLogger(),
  withChaos(),
  withTimeout(),
  withContext(),
)(async (event) => {
  return {
    ok: true,
    event,
  };
});

Use factory(...) to create a custom Lambda wrapper:

import { compose, factory, withError, withLogger, withTimeout } from '@vyriy/handler';

const withRequestId = factory<{ headerName?: string }>(async (handler, args, options = {}) => {
  const [event] = args;
  const headerName = options.headerName ?? 'x-request-id';
  const requestId =
    typeof event === 'object' && event && 'headers' in event
      ? (event.headers as Record<string, string | undefined> | undefined)?.[headerName]
      : undefined;

  if (requestId) {
    console.info('Request ID:', requestId);
  }

  return handler(...args);
});

export const handler = compose(
  withError(),
  withLogger(),
  withTimeout(),
  withRequestId({
    headerName: 'x-request-id',
  }),
)(async (event) => {
  return {
    ok: true,
    event,
  };
});

Each runtime has matching helpers:

  • Lambda: compose and factory from @vyriy/handler
  • HTTP: httpCompose and httpFactory from @vyriy/handler, or compose / factory from @vyriy/handler/api/http/compose and @vyriy/handler/api/http/factory
  • Stream: streamCompose and streamFactory from @vyriy/handler, or compose / factory from @vyriy/handler/api/stream/compose and @vyriy/handler/api/stream/factory

Wrappers

Wrapper exports are split by runtime:

import { withError, withLogger } from '@vyriy/handler/wrappers';
import { withHeaders } from '@vyriy/handler/api/wrappers';
import { httpWithHeaders } from '@vyriy/handler/api/http/wrappers';
import { streamWithHeaders } from '@vyriy/handler/api/stream/wrappers';

The root @vyriy/handler entrypoint re-exports the public wrappers for convenience.

Common Lambda Wrappers

withError(options?)

Catches handler failures, optionally runs a side-effect errorHandler, and rethrows the original error.

Options:

{
  errorHandler?: (error: unknown, args: HandlerParams<Event>) => Promise<void> | void;
}

withLogger(options?)

Logs the incoming event and context, then logs either the result or the thrown error.

Options:

{
  logger?: typeof console;
}

By default the wrapper creates a logger via @vyriy/logger.

withTimeout()

Races the handler against a timeout scheduled one second before the Lambda runtime limit.

withContext()

Sets context.callbackWaitsForEmptyEventLoop = false before calling the handler.

withSmoke()

Returns the smoke response when the incoming event has isSmoke: true. Matching is delegated to @vyriy/smoke.

withChaos(options?)

Injects development-only random failures before the wrapped handler runs.

Options:

{
  enabled?: boolean;
  probability?: number;
  strategy?: 'error' | 'timeout' | 'random';
  timeoutMs?: number;
  error?: unknown;
}

enabled and timeoutMs can be read through @vyriy/config when they are not passed directly.

API Wrappers

withApiError(options?)

Catches API handler failures and converts them to an API Gateway result. Without a custom errorHandler, it returns a JSON 500.

Options:

{
  errorHandler?: (error: unknown, args: HandlerParams<ApiEvent>) => Promise<ApiResult> | ApiResult;
}

withHealthcheck(options?)

Returns a JSON 200 response when event.path matches the configured path.

Options:

{
  path?: string;
  action?: () => Promise<void>;
}

withHeaders(options?)

Adds configured headers to the API result.

Options:

Record<string, string>;

withCors()

Short-circuits API Gateway OPTIONS requests with a 204 response and delegates all other requests.

HTTP Wrappers

httpWithError(options?)

Catches native HTTP handler failures and runs an optional side-effect errorHandler. When the response is still open it writes a JSON 500; when headers were already sent it only ends the response.

Options:

{
  errorHandler?: (error: unknown, args: HttpHandlerParams) => Promise<void> | void;
}

httpWithLogger(options?)

Logs the incoming request method and URL, then logs either the final response status code or the thrown error. Accepts the same logger option as withLogger.

httpWithHealthcheck(options?)

Writes a JSON 200 response when the request path matches the configured path.

Options:

{
  path?: string;
  action?: () => Promise<void>;
  body?: unknown;
}

httpWithHeaders(options?)

Sets configured headers on the response before delegating, so handler-defined headers win on key conflicts.

Options:

Record<string, string>;

httpWithCors()

Short-circuits OPTIONS preflight requests with a 204 response and delegates all other requests.

Stream Wrappers

The streamWith* wrappers mirror the API and common Lambda wrappers for response streaming handlers:

  • streamWithApiError(options?)
  • streamWithLogger(options?)
  • streamWithTimeout()
  • streamWithContext()
  • streamWithSmoke()
  • streamWithHealthcheck(options?)
  • streamWithHeaders(options?)
  • streamWithCors()
  • streamWithChaos(options?)

Stream-specific wrappers write metadata through the response stream before or instead of delegating to the wrapped stream handler.

Types

The package exports shared Lambda handler types from the root entrypoint:

import type { Context, Decorator, Handler, HandlerParams, Response } from '@vyriy/handler';

Runtime-specific types are available from their runtime entrypoints:

import type { ApiEvent, ApiOptions, ApiResult } from '@vyriy/handler/api';
import type { HttpApiOptions, HttpDecorator, HttpHandler, HttpHandlerParams } from '@vyriy/handler/api/http';
import type { ResponseStream, StreamApiOptions, StreamHandler } from '@vyriy/handler/api/stream';

The root entrypoint still re-exports runtime-specific types for compatibility.

Notes

  • api includes API-specific wrappers such as healthcheck handling, default headers, and CORS preflight handling
  • httpApi skips timeout, context, smoke, and chaos wrappers because they rely on the Lambda context and event shape
  • streamApi keeps the API-style behavior but adapts result writing to Lambda response streams
  • dynamodb, s3, ses, schedule, sns, sqs, and eventBridge use withError() so failures are rethrown for event-source retry behavior
  • Passing headers to create.api, create.httpApi, or create.streamApi replaces that chain's default headers