Zuplo is fully programmable. You can write custom TypeScript code for inbound
policies, outbound policies, and request handlers to extend every part of your
API gateway. This guide covers the function signatures, common patterns, and
best practices for writing custom code.
Custom Inbound Policy
An inbound policy runs before the request handler. It receives the incoming
request and can modify it, pass it along, or short-circuit the pipeline by
returning a Response.
import { ZuploContext, ZuploRequest } from "@zuplo/runtime";export default async function ( response: Response, request: ZuploRequest, context: ZuploContext,): Promise<Response> { // Create a new response to get mutable headers const newResponse = new Response(response.body, { status: response.status, headers: response.headers, }); newResponse.headers.set("x-request-id", context.requestId); newResponse.headers.set("x-tenant-id", context.custom.tenantId ?? "unknown"); return newResponse;}
Register outbound policies the same way as inbound, using custom-code-outbound
as the policyType. See
Custom Code Outbound Policy for the full
reference.
Custom Request Handler
A request handler is the function that produces the response for a route. Use a
custom handler when you need to orchestrate calls, build a BFF
(backend-for-frontend), or return a fully custom response.
Both inbound and outbound policies receive an options parameter populated from
the handler.options object in config/policies.json. Define a TypeScript
interface for type safety:
modules/rate-limit-by-plan.ts
import { ZuploContext, ZuploRequest, HttpProblems } from "@zuplo/runtime";interface PolicyOptions { defaultLimit: number; premiumLimit: number;}export default async function ( request: ZuploRequest, context: ZuploContext, options: PolicyOptions, policyName: string,): Promise<ZuploRequest | Response> { const plan = request.user?.data?.plan ?? "free"; const limit = plan === "premium" ? options.premiumLimit : options.defaultLimit; context.custom.rateLimit = limit; context.log.info(`Applying rate limit of ${limit} for plan '${plan}'`); return request;}
Use context.custom to share data between policies and the request handler
within a single request. It is a mutable object that lives for the lifetime of
the request.
// Handler: read metadata set by the policyexport default async function ( request: ZuploRequest, context: ZuploContext,): Promise<Response> { const userId = context.custom.userId; const plan = context.custom.plan; context.log.info(`Handling request for user ${userId} on plan ${plan}`); // ...build your response return new Response(JSON.stringify({ userId, plan }), { status: 200, headers: { "content-type": "application/json" }, });}
For the full ZuploContext reference, see
ZuploContext.
Accessing Environment Variables
Import environment from @zuplo/runtime to read environment variables in any
custom module:
Always validate required variables early to surface configuration errors at
startup rather than at request time:
Code
import { environment, ConfigurationError } from "@zuplo/runtime";const apiKey = environment.API_KEY;if (!apiKey) { throw new ConfigurationError("API_KEY environment variable is not set");}
Reshape a backend response before returning it to the client:
modules/transform-response.ts
import { ZuploContext, ZuploRequest } from "@zuplo/runtime";export default async function ( response: Response, request: ZuploRequest, context: ZuploContext,): Promise<Response> { if (response.status !== 200) { return response; } const data = await response.json(); // Wrap the response in an envelope const envelope = { success: true, data, metadata: { requestId: context.requestId, timestamp: new Date().toISOString(), }, }; return new Response(JSON.stringify(envelope), { status: 200, headers: { "content-type": "application/json" }, });}
When to Use Policy vs Handler vs Hook
Zuplo provides three extension points for custom code. Choose the right one
based on your use case:
Extension Point
Use When
Inbound Policy
You need to inspect, validate, or modify the request before it reaches the handler.
Outbound Policy
You need to inspect or transform the response after the handler runs.
Request Handler
You need to produce the response -- call backends, aggregate data, or return custom responses.
Hook
You need cross-cutting logic that applies globally, such as logging or tracing.
Policies are configured per-route and are reusable across multiple routes. Hooks
are registered globally in zuplo.runtime.ts and run on every request. For a
detailed explanation of how these fit together, see
Request Lifecycle.