@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/handlerWith Yarn:
yarn add @vyriy/handlerFor TypeScript Lambda projects, install AWS Lambda types as a development dependency:
yarn add @types/aws-lambdaThe 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:
composeandfactoryfrom@vyriy/handler - HTTP:
httpComposeandhttpFactoryfrom@vyriy/handler, orcompose/factoryfrom@vyriy/handler/api/http/composeand@vyriy/handler/api/http/factory - Stream:
streamComposeandstreamFactoryfrom@vyriy/handler, orcompose/factoryfrom@vyriy/handler/api/stream/composeand@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
apiincludes API-specific wrappers such as healthcheck handling, default headers, and CORS preflight handlinghttpApiskips timeout, context, smoke, and chaos wrappers because they rely on the Lambda context and event shapestreamApikeeps the API-style behavior but adapts result writing to Lambda response streamsdynamodb,s3,ses,schedule,sns,sqs, andeventBridgeusewithError()so failures are rethrown for event-source retry behavior- Passing
headerstocreate.api,create.httpApi, orcreate.streamApireplaces that chain's default headers
