Skip to content

Handlers

The handler() function from @venturekit/runtime is the primary API for writing route handlers. It wraps your business logic with authentication, body parsing, status code detection, middleware, and error handling.

import { handler } from '@venturekit/runtime';
// Public endpoint (no scopes = no auth required)
export const main = handler(async (_body, ctx, logger) => {
logger.info('Health check');
return { status: 'healthy', timestamp: ctx.timestamp.toISOString() };
});

Add scopes to require authentication and specific OAuth scope permissions:

export const main = handler(async (body, ctx, logger) => {
logger.info('Creating task', { userId: ctx.user?.id });
return { id: '123', title: body.title };
}, { scopes: ['tasks.write'] });

When scopes are specified:

  1. The handler checks for a valid JWT in the request
  2. If no JWT is present, it returns 401 Unauthorized
  3. If the JWT doesn’t contain all required scopes, it returns 403 Forbidden
handler<TBody, TResult>(
fn: (body: TBody, ctx: RequestContext, logger: Logger) => Promise<TResult>,
config?: HandlerConfig
)

Parameters:

  • body — Parsed JSON request body (auto-parsed from the event)
  • ctxRequest context with metadata, user info, and more
  • logger — Structured logger (Pino-based)

Type parameters:

  • TBody — Type of the request body
  • TResult — Type of the return value
interface CreateTaskBody {
title: string;
}
interface Task {
id: string;
title: string;
}
export const main = handler<CreateTaskBody, Task>(
async (body, _ctx, logger) => {
logger.info('Creating task', { title: body.title });
return { id: Date.now().toString(), title: body.title };
},
{ scopes: ['tasks.write'] }
);

Status codes are auto-detected from the HTTP method:

HTTP MethodDefault Status
GET200 OK
PUT200 OK
PATCH200 OK
POST201 Created
DELETE204 No Content

Override explicitly:

export const main = handler(async (body, ctx, logger) => {
return { accepted: true };
}, { status: 200 }); // Force 200 instead of auto-detected
interface HandlerConfig {
scopes?: string[]; // Required OAuth scopes (empty = public)
status?: 200 | 201 | 204; // Explicit status code
middleware?: Middleware[]; // Custom middleware
logLevel?: 'debug' | 'info' | 'warn' | 'error';
transactional?: boolean; // Wrap in a database transaction
}

Every handler receives a RequestContext:

interface RequestContext {
requestId: string; // Unique request ID for tracing
timestamp: Date; // Request timestamp
method: string; // HTTP method
path: string; // Request path
sourceIp: string; // Client IP
userAgent: string; // User agent
user: UserContext | null; // Authenticated user (or null)
tenant: TenantContext | null;// Tenant (when @venturekit-pro/tenancy is used)
locale: string; // Resolved locale
queryParams?: Record<string, string | undefined>;
tx?: unknown; // Database transaction (transactional handlers)
intentOutputs?: Record<string, unknown>; // Infrastructure outputs
rawEvent: APIGatewayProxyEventV2; // Raw AWS event
}

When a request is authenticated:

interface UserContext {
id: string; // Cognito sub
email?: string; // User email
scopes: string[]; // OAuth scopes
claims: Record<string, unknown>; // All JWT claims
}

Wrap your handler in a database transaction (requires @venturekit/data):

export const main = handler(async (body, ctx, logger) => {
// ctx.tx is a transaction — auto-commits on success, rolls back on error
await ctx.tx.query('INSERT INTO tasks (title) VALUES ($1)', [body.title]);
await ctx.tx.query('UPDATE counters SET count = count + 1');
return { created: true };
}, { scopes: ['tasks.write'], transactional: true });

Add middleware to individual handlers:

import { handler, corsMiddleware, timeoutMiddleware } from '@venturekit/runtime';
export const main = handler(async (_body, ctx, logger) => {
return { ok: true };
}, {
middleware: [
timeoutMiddleware(5000),
],
});

See Middleware for details on built-in and custom middleware.