Skip to content

Middleware

VentureKit provides a composable middleware system for cross-cutting concerns like logging, CORS, timeouts, and error handling.

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 → Response

Catches all errors and formats them into structured JSON responses. This is applied automatically to every handler.

Logs request start, completion, and failures with timing information:

import { loggingMiddleware, createLogger } from '@venturekit/runtime';
const logger = createLogger({ minLevel: 'info' });
const logging = loggingMiddleware(logger);

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,
});

Enforces a maximum request duration:

import { timeoutMiddleware } from '@venturekit/runtime';
const timeout = timeoutMiddleware(5000); // 5 seconds

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),
],
});

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 memory
const rateLimit = rateLimitMiddleware({
check: await createDefaultRateLimiter(),
});
// Strict preset for auth endpoints — 20 req/min
const authRateLimit = rateLimitMiddleware({
check: await createDefaultRateLimiter({ preset: 'strict' }),
});

Presets:

PresetLimitWindowUse Case
relaxed200/min60sInternal APIs, dev
standard100/min60sPublic APIs (default)
strict20/min60sAuth, sensitive ops
api-key1000/min60sAuthenticated consumers

In production, rate limit counters are stored in DynamoDB with atomic increments and TTL cleanup. See the Lambda Invocation guide for more.

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 errors
const 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.

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;
},
};

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() }),
]);

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),
}),
],
});