Middleware
VentureKit provides a composable middleware system for cross-cutting concerns like logging, CORS, timeouts, and error handling.
How Middleware Works
Section titled “How Middleware Works”Middleware wraps handler execution in a stack pattern. Each middleware can:
- Run code before the handler
- Run code after the handler
- Catch and handle errors
- Short-circuit the request (return a response without calling the handler)
Request → ErrorBoundary → Logging → Custom Middleware → Handler → ResponseBuilt-in Middleware
Section titled “Built-in Middleware”Error Boundary
Section titled “Error Boundary”Catches all errors and formats them into structured JSON responses. This is applied automatically to every handler.
Logging Middleware
Section titled “Logging Middleware”Logs request start, completion, and failures with timing information:
import { loggingMiddleware, createLogger } from '@venturekit/runtime';
const logger = createLogger({ minLevel: 'info' });const logging = loggingMiddleware(logger);CORS Middleware
Section titled “CORS Middleware”Adds CORS headers and handles preflight OPTIONS requests:
import { corsMiddleware } from '@venturekit/runtime';
const cors = corsMiddleware({ allowOrigins: ['https://app.example.com'], allowMethods: ['GET', 'POST', 'PUT', 'DELETE'], allowHeaders: ['Content-Type', 'Authorization'], allowCredentials: true, maxAge: 3600,});Timeout Middleware
Section titled “Timeout Middleware”Enforces a maximum request duration:
import { timeoutMiddleware } from '@venturekit/runtime';
const timeout = timeoutMiddleware(5000); // 5 secondsUsing Middleware in Handlers
Section titled “Using Middleware in Handlers”Pass middleware in the handler config:
import { handler, corsMiddleware, timeoutMiddleware } from '@venturekit/runtime';
export const main = handler(async (_body, ctx, logger) => { return { ok: true };}, { middleware: [ timeoutMiddleware(5000), ],});Rate Limiting
Section titled “Rate Limiting”Built-in rate limiting with distributed DynamoDB storage (production) or in-memory (dev):
import { rateLimitMiddleware } from '@venturekit/runtime';import { createDefaultRateLimiter } from '@venturekit/runtime/rate-limit';
// Zero-config — 100 req/min per IP, auto-selects DynamoDB or memoryconst rateLimit = rateLimitMiddleware({ check: await createDefaultRateLimiter(),});
// Strict preset for auth endpoints — 20 req/minconst authRateLimit = rateLimitMiddleware({ check: await createDefaultRateLimiter({ preset: 'strict' }),});Presets:
| Preset | Limit | Window | Use Case |
|---|---|---|---|
relaxed | 200/min | 60s | Internal APIs, dev |
standard | 100/min | 60s | Public APIs (default) |
strict | 20/min | 60s | Auth, sensitive ops |
api-key | 1000/min | 60s | Authenticated consumers |
In production, rate limit counters are stored in DynamoDB with atomic increments and TTL cleanup. See the Lambda Invocation guide for more.
Fail-closed by default
Section titled “Fail-closed by default”If the DynamoDB store errors out (network blip, missing permissions, table
missing), the limiter fails closed — it denies the request rather than
letting an outage of the limiter open the gate on sensitive endpoints. Each
failure emits a single structured warn log with the stable code
rate_limit_store_error so dashboards can alert on it.
Opt into fail-open behavior for best-effort public APIs where a limiter outage shouldn’t DoS your own users:
// Per-limiter: allow requests through when the store errorsconst rateLimit = rateLimitMiddleware({ check: await createDefaultRateLimiter({ onError: 'open' }),});Or set VENTURE_RATE_LIMIT_ON_ERROR=open to flip the default globally. An
explicit onError always wins over the env var.
Writing Custom Middleware
Section titled “Writing Custom Middleware”A middleware is an object with a name and fn:
import type { Middleware } from '@venturekit/runtime';
const myMiddleware: Middleware = { name: 'myMiddleware', fn: async (ctx, next) => { // Run before handler console.log('Before:', ctx.path);
// Call the next middleware or handler const response = await next();
// Run after handler (optional) return response; },};Composing Middleware
Section titled “Composing Middleware”Use compose() to combine multiple middleware into a single function:
import { compose, rateLimitMiddleware } from '@venturekit/runtime';import { createDefaultRateLimiter } from '@venturekit/runtime/rate-limit';
// Each entry must be a `Middleware`, not a factory — call every middleware// constructor with its options.const stack = compose([ loggingMiddleware(logger), corsMiddleware({ /* ... */ }), timeoutMiddleware(5000), rateLimitMiddleware({ check: await createDefaultRateLimiter() }),]);Integration with Tenancy and Auth
Section titled “Integration with Tenancy and Auth”Feature packages provide their own middleware:
import { createTenantMiddleware, createQuotaMiddleware } from '@venturekit-pro/tenancy';
export const main = handler(async (_body, ctx, logger) => { // ctx.tenant is populated by tenant middleware return { tenantId: ctx.tenant?.id };}, { scopes: ['api.read'], middleware: [ createTenantMiddleware({ // `strategy` selects the data isolation model; `resolution` selects how the // tenant id is extracted from each request. config: { strategy: 'shared', resolution: 'subdomain' }, lookupTenant: async (id) => loadTenant(id), }), createQuotaMiddleware({ quotaKey: 'apiRequests', checkUsage: async (tenantId) => getMonthlyUsage(tenantId), }), ],});