The ZuploContext object provides information about the current request and
runtime environment. It is passed as the second parameter to request handlers
and policies.
import { ZuploContext, ZuploRequest } from "@zuplo/runtime";export default async function (request: ZuploRequest, context: ZuploContext) { // Access context properties and methods context.log.info(`Processing request ${context.requestId}`); return new Response("Hello World");}
ts
Properties
contextId
A unique identifier for the current execution context. This is different from
requestId and is useful for correlating multiple operations within the same
execution.
A logger instance for debugging and monitoring. Logs appear in your log tail in
the portal and in your integrated log solution (e.g. DataDog). Pre-production
environments are typically set to Info log level, while production is set to
Error.
As an alternative to using the log property, you can also use console.log,
console.error, etc. to log messages. See the
documentation for more details.
Reference to the parent context when using invokeRoute. This property is
undefined for the initial request context. This is useful for detecting
sub-requests and accessing parent context data.
if (context.parentContext) { context.log.info("This is a sub-request"); const parentRequestId = context.parentContext.requestId; context.log.info(`Parent request: ${parentRequestId}`);}
ts
requestId
A UUID for every request. This is used in logging and can be handy to tie events
together. The requestId is automatically logged with every use of the logger.
The request ID is also included in the zp-rid header of the responses from
your API for diagnostic and tracing purposes.
A read-only pointer to the configuration for the matched route. Includes the
label, path, methods supported, name of the version, and names of policies. This
type is immutable - the routing table cannot be updated at runtime.
Adds a hook that executes before a response is sent to the client. This hook can
modify the response before it is sent. Multiple hooks can be added and they
execute in the order they were added.
Adds a hook that executes after all other response processing is complete but
before the response is sent. Unlike addResponseSendingHook, this hook cannot
modify the response - it is for monitoring and logging purposes only.
Programmatically executes an inbound policy from your policy library. This is
useful for conditionally executing policies based on request attributes.
import { ZuploContext, ZuploRequest } from "@zuplo/runtime";export default async function (request: ZuploRequest, context: ZuploContext) { // Conditionally apply rate limiting if (request.user.data.isFreeUser) { const result = await context.invokeInboundPolicy( "rate-limit-policy", request, ); if (result instanceof Response) { return result; // Rate limit exceeded } request = result; // Use the new request object } // Continue processing return fetch(request);}
ts
Important Notes:
The method returns either a Request or Response object
A Response indicates the policy wants to short-circuit and stop processing
A Request is typically a new request object created by the policy
The original request becomes locked after invoking a policy
const result = await context.invokeInboundPolicy("my-policy", request);if (result instanceof Response) { // Policy terminated the request chain context.log.warn(`Policy returned status: ${result.status}`); return result;}// Continue with the new request objectreturn fetch(result);
ts
invokeOutboundPolicy
Programmatically executes an outbound policy from your policy library. Outbound
policies process responses before they are sent to the client.
export default async function (request: ZuploRequest, context: ZuploContext) { // Make the upstream request const response = await fetch(request); // Conditionally apply response transformation if (response.headers.get("content-type")?.includes("application/json")) { return context.invokeOutboundPolicy( "json-transform-policy", response, request, ); } // Apply caching policy for successful responses if (response.ok) { return context.invokeOutboundPolicy("cache-policy", response, request); } return response;}
ts
invokeRoute
Invokes another route within the same API gateway. This enables internal routing
and composition of multiple routes. The invoked route runs with a new context
that has parentContext set to the current context.
export default async function (request: ZuploRequest, context: ZuploContext) { // First, validate the request using an internal route const validationResponse = await context.invokeRoute("/internal/validate", { method: "POST", body: JSON.stringify({ userId: request.user?.sub, resource: request.params.resourceId, action: "read", }), headers: { "Content-Type": "application/json", }, }); if (!validationResponse.ok) { return new Response("Access denied", { status: 403 }); } // Then fetch user preferences from another internal route const prefsResponse = await context.invokeRoute( `/internal/users/${request.user?.sub}/preferences`, ); const preferences = await prefsResponse.json(); // Continue with main processing using the preferences const response = await fetch(request); // Apply preferences to response if (preferences.format === "xml") { return context.invokeOutboundPolicy("json-to-xml", response, request); } return response;}
ts
waitUntil
Notifies the runtime to keep the process alive until the provided promise
resolves. This is essential for asynchronous work that continues after sending a
response, such as logging, analytics, or cleanup tasks.
export default async function (request: ZuploRequest, context: ZuploContext) { const startTime = Date.now(); const response = await fetch(request); // Asynchronous work that continues after response const asyncWork = async () => { try { // Log to external service await fetch("https://analytics.example.com/api/track", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${context.secrets.ANALYTICS_API_KEY}`, }, body: JSON.stringify({ event: "api_call", properties: { path: request.url, method: request.method, status: response.status, duration: Date.now() - startTime, country: context.incomingRequestProperties.country, userId: request.user?.sub, }, timestamp: new Date().toISOString(), }), }); } catch (error) { context.log.error("Failed to send analytics", error); } }; // Tell runtime to wait for async work to complete context.waitUntil(asyncWork()); return response;}
ts
Common Patterns
Request Timing
export default async function (request: ZuploRequest, context: ZuploContext) { // Store start time context.custom.startTime = Date.now(); // Add response hook to measure duration context.addResponseSendingHook(async (response) => { const duration = Date.now() - context.custom.startTime; response.headers.set("X-Response-Time", `${duration}ms`); return response; }); return fetch(request);}