Skip to content

@venturekit/runtime API

Creates a unified Lambda handler that wraps your business logic with auth, body parsing, status codes, middleware, and error handling.

function handler<TBody = unknown, TResult = unknown>(
fn: HandlerFn<TBody, TResult>,
config?: HandlerConfig
): (event: APIGatewayProxyEventV2, context: Context) => Promise<APIGatewayProxyResultV2>

Returns a drop-in replacement for handler with app-wide defaults baked in — so every route stops repeating the same middleware stack (tenant resolution, session auth, per-tenant scope grants). Define it once and import the bound handler across your routes.

function createHandler(defaults: HandlerDefaults): BoundHandler
interface HandlerDefaults {
middleware?: Middleware<RequestContext>[] // prepended to each route's own middleware
logLevel?: 'debug' | 'info' | 'warn' | 'error' // fallback when the route sets none
}
src/lib/handler.ts
import { createHandler, setDefaultAuthMiddleware } from '@venturekit/runtime';
setDefaultAuthMiddleware(cookieAuthMiddleware()); // scoped routes
export const apiHandler = createHandler({
middleware: [tenancyMiddleware, memberScopesMiddleware()],
});
// src/routes/....ts — routes shrink to their essence
export const main = apiHandler(async (_body, ctx) => { /* … */ }, {
scopes: ['admin.members.read'],
});

Merge semantics: middleware runs defaults-first ([...defaults, ...route]); the default auth middleware still runs before both; logLevel — the route value wins; everything else is per-route. Routes that must NOT inherit the defaults (health checks, cross-tenant/platform endpoints, webhook receivers) keep using the raw handler.

Overrides the middleware the runtime injects to populate ctx.user on scoped routes. By default the runtime lazily imports @venturekit/auth/server and uses its cookieAuthMiddleware(); if that package isn’t installed it falls back to the API Gateway Cognito Authorizer (claims in event.requestContext.authorizer.jwt.claims). Pass null to disable auth-middleware injection entirely.

function setDefaultAuthMiddleware(mw: Middleware<RequestContext> | null): void
function _resetDefaultAuthMiddlewareForTesting(): void // test-only: clears the resolution cache

Timing: each handler() / createHandler() factory caches the resolved auth middleware on its first request — call this in app bootstrap (or before invoking test handlers) so it takes effect.

Creates a handler for non-HTTP Lambda functions (standalone functions, queue consumers, scheduled tasks). Provides middleware, logging, validation, transactions, timeout, and application-level retries.

function taskHandler<TEvent = unknown, TResult = unknown>(
fn: TaskHandlerFn<TEvent, TResult>,
config?: TaskHandlerConfig<TEvent>
): (event: TEvent, context: Context) => Promise<TResult>

Builds a RequestContext from an API Gateway event.

function buildContext(
event: APIGatewayProxyEventV2,
options: { supportedLocales: string[]; defaultLocale: string }
): RequestContext

Extracts user information from JWT claims in the API Gateway event.

function extractUserContext(event: APIGatewayProxyEventV2): UserContext | null

extractLocale(event, supportedLocales, defaultLocale)

Section titled “extractLocale(event, supportedLocales, defaultLocale)”

Extracts the locale from the Accept-Language header.

function extractLocale(event: APIGatewayProxyEventV2, supported: string[], fallback: string): string

Returns the browser-facing host of a request — what the address bar shows — for cookie Domain decisions and redirect URLs. Accepts a RequestContext (reads ctx.rawEvent) or a raw API Gateway event.

function getRequestHost(
source: RequestContext | { headers?: Record<string, string | undefined> } | null | undefined,
): string

Header priority is originrefererhostx-forwarded-host (origin/referer outrank host so a same-origin SPA proxy resolves to the browser’s host, not the API’s). Full-URL values are reduced to their host (https://app.example.com/loginapp.example.com, port kept); bare host values (app.example.com:3000) pass through as-is. Returns '' when no usable header is present (the opaque Origin: null is skipped).

Appends a fully-formed Set-Cookie header value to the outgoing response. The runtime drains ctx.cookies into the API Gateway HTTP API v2 cookies field — one Set-Cookie per entry. Formatting is the caller’s job; use @venturekit/auth/server’s buildSessionCookies / buildClearSessionCookies / buildOAuthStateCookie to produce well-formed strings.

function setCookie(ctx: BaseContext, value: string): void
FunctionStatusDescription
success(data, meta?)200Success response
created(data, meta?)201Created response
noContent(meta?)204No content response
error(statusCode, body)*Custom error response
errorResponse(error, meta?)*Format a VentureError
redirect(url, statusCode?)301/302Redirect response

By default handler() wraps whatever a route returns in the JSON success envelope ({ data, meta }). For browser-navigation endpoints that need full control of status/headers/body — OAuth redirects, session handoffs, HTML — return a pre-built result through rawResult() so the runtime passes it through untouched (cookies set via setCookie are still attached):

function rawResult(result: APIGatewayProxyResultV2): RawResult // mark for pass-through
function isRawResult(value: unknown): value is RawResult // detect a wrapped result
export const main = handler(async (_body, ctx) => {
setCookie(ctx, sessionCookie);
return rawResult(redirect('/dashboard'));
});
FunctionDescription
compose(middlewares)Compose middleware into a single function
loggingMiddleware(logger)Request/response logging with timing
corsMiddleware(options)CORS headers and preflight handling
timeoutMiddleware(ms)Request timeout enforcement
errorBoundaryMiddleware(handler)Catch and format errors
rateLimitMiddleware(options)Rate limiting with pluggable stores
requestIdMiddleware()Ensure request ID is set
timingMiddleware()Add X-Response-Time header
apiKeyAuthMiddleware(options)API key authentication
ExportDescription
LoggerLogger class
createLogger(config?)Create a new logger instance
loggerDefault logger instance
type HandlerFn<TBody, TResult> = (
body: TBody,
ctx: RequestContext,
logger: Logger
) => Promise<TResult>
interface HandlerConfig<TBody = unknown, TQuery = unknown, TParams = unknown> {
scopes?: string[]
status?: 200 | 201 | 204
middleware?: Middleware[]
logLevel?: 'debug' | 'info' | 'warn' | 'error'
transactional?: boolean
body?: ZodLikeSchema<TBody> // Zod schema for request body validation
query?: ZodLikeSchema<TQuery> // Zod schema for query parameter validation
params?: ZodLikeSchema<TParams> // Zod schema for path parameter validation
}

When a Zod schema is provided, the input is validated before the handler runs. On failure, a 422 ValidationError is thrown with structured field errors.

interface RequestContext {
requestId: string
timestamp: Date
method: string
path: string
sourceIp: string
userAgent: string
user: UserContext | null
tenant: TenantContext | null
locale: string
queryParams?: Record<string, string | undefined>
pathParams?: Record<string, string | undefined>
tx?: unknown
intentOutputs?: Record<string, unknown>
isInternal: boolean
invokeId?: string
cookies: string[]
rawEvent: APIGatewayProxyEventV2
}
  • isInternaltrue when this request came from another VentureKit function via invoke(), false for external API Gateway requests. Use this to skip rate limiting, auth, or other external-only logic.
  • invokeId — The unique invoke ID for internal calls, useful for tracing function-to-function calls.
  • pathParams — Route [param] segments (e.g. { id: '123' } for GET /users/{id}), overwritten with the parsed value when a params schema is supplied. Read as ctx.pathParams?.['id'].
  • cookies — Outgoing Set-Cookie values; append via setCookie(ctx, …). Drained into the HTTP API v2 response by the runtime.
interface UserContext {
id: string
email?: string
scopes: string[]
claims: Record<string, unknown>
}
interface TenantContext {
id: string
slug?: string
metadata: Record<string, unknown>
}

Generic middleware type that works with both HTTP handlers and task handlers. Middleware written against BaseContext is reusable across all handler types.

interface Middleware<TCtx extends BaseContext = BaseContext> {
name: string
fn: MiddlewareFn<TCtx>
}
type MiddlewareFn<TCtx extends BaseContext = BaseContext> = (
ctx: TCtx,
next: () => Promise<any>
) => Promise<any>
// This middleware works in both handler() and taskHandler()
const timing: Middleware = {
name: 'timing',
fn: async (ctx, next) => {
const start = Date.now();
const result = await next();
console.log(`${ctx.requestId} took ${Date.now() - start}ms`);
return result;
},
};

Shared fields available in all handler types:

interface BaseContext {
requestId: string
timestamp: Date
tx?: Transaction
intentOutputs?: Record<string, unknown>
traceId?: string
}

RequestContext extends BaseContext with HTTP-specific fields. TaskContext extends BaseContext with Lambda-specific fields (functionName, event, logger).

interface TaskContext extends BaseContext {
functionName: string
getRemainingTimeInMillis: () => number
rawContext: Context
event: unknown
logger: Logger
}
interface TaskHandlerConfig<TEvent = unknown> {
middleware?: Middleware<TaskContext>[]
logLevel?: 'debug' | 'info' | 'warn' | 'error'
transactional?: boolean
event?: ZodLikeSchema<TEvent> // Zod schema for event validation
timeout?: number // Timeout in ms (default: none, Lambda timeout applies)
retries?: number // Application-level retries within one invocation (default: 0)
retryDelayMs?: number // Initial retry delay in ms, doubles each attempt (default: 1000)
}

Thrown when event validation fails. Never retried by the application retry loop.

class TaskValidationError extends Error {
readonly issues: Array<{ path: (string | number)[]; message: string }>
}
ClassStatusCode
VentureError500Base class
BadRequestError400BAD_REQUEST
UnauthorizedError401UNAUTHORIZED
ForbiddenError403FORBIDDEN
NotFoundError404NOT_FOUND
ConflictError409CONFLICT
PayloadTooLargeError413PAYLOAD_TOO_LARGE
ValidationError422VALIDATION_ERROR
RateLimitError429RATE_LIMITED
InternalError500INTERNAL_ERROR
ServiceUnavailableError503SERVICE_UNAVAILABLE

Type guard to check if an error is a VentureError.

function isVentureError(error: unknown): error is VentureError

Unified API for calling other VentureKit functions. Works locally and in production.

// Route invocation
const users = await invoke<User[]>('users/list', { method: 'GET' });
// Function invocation
const result = await invoke({ function: 'process-orders' }, {
payload: { orderId: 'abc' },
});
// Fire-and-forget
await invoke('notifications/send', { mode: 'async', body: { userId: '42' } });

Auto-propagation: invoke() automatically propagates trace context, tenant ID, and user ID. You can override any of these explicitly.

OptionDescription
methodHTTP method (default: POST)
bodyRequest body
pathParamsPath parameters
queryParamsQuery parameters
headersAdditional headers
mode'sync' (default) or 'async'
traceTrace context (auto-propagated from headers)
tenantIdTenant ID (auto-propagated from context)
userIdUser ID (auto-propagated from context)
fallbackDefault result when the target is unavailable (enables circuit breaker)

Add a single fallback param to protect against cascading failures. VentureKit automatically tracks errors per target — after 5 consecutive failures the circuit “opens” and returns your fallback instantly, without calling the target. After 30 seconds it probes once to see if the target recovered.

// Route call with fallback — one extra param, that's it
const users = await invoke<User[]>('users/list', {
method: 'GET',
fallback: { statusCode: 503, data: [], headers: {} },
});
// Function call with fallback
const result = await invoke(
{ function: 'process-orders', project: 'billing' },
{ payload: { orderId: '123' }, fallback: { statusCode: 503, data: null, headers: {} } },
);

No setup required — the circuit breaker is built-in. If you need custom thresholds:

import { configureCircuitBreaker } from '@venturekit/runtime/patterns';
// Optional: customize globally (call once at startup)
configureCircuitBreaker({
failureThreshold: 3, // open after 3 failures (default: 5)
resetTimeoutMs: 60_000, // probe after 60s (default: 30s)
});

You can also use createCircuitBreaker() directly for advanced use cases outside of invoke().

Call functions in other VentureKit projects (microservices):

// Call 'send-invoice' in the 'billing' project
const result = await invoke(
{ function: 'send-invoice', project: 'billing' },
{ payload: { orderId: '123' } },
);

The resolved Lambda name becomes {project}-{stage}-fn-{name}.

For local development, set VENTURE_LOCAL_URLS so each project’s dev server is reachable:

Terminal window
VENTURE_LOCAL_URLS="app=http://localhost:3000,billing=http://localhost:3001"

Creates a LambdaInvoker that routes calls to the local dev server via HTTP. Used automatically when VENTURE_LOCAL=true.

Supports cross-project routing via VENTURE_LOCAL_URLS — matches the function name prefix against the registry and routes to the correct project’s dev server.

Import from the /ws subpath:

import { connectionStore } from '@venturekit/runtime/ws';
MethodDescription
save(connectionId)Save a new connection (unauthenticated)
authenticate(connectionId, metadata)Upgrade to authenticated
remove(connectionId)Remove a connection
get(connectionId)Get a connection record
getAll()Get every authenticated connection (prefer getByUser / getByTenant at scale)
getByUser(userId)Get all connections for a user
getByTenant(tenantId)Get all connections for a tenant
postToConnection(domain, stage, connId, data)Send to one connection
sendToUser(domain, stage, userId, data)Send to all user sessions
sendToTenant(domain, stage, tenantId, data)Send to all tenant connections
broadcast(domain, stage, data)Send to all connections