Hooks allow you to run code at specific points in the request/response pipeline.
They're accessible through the ZuploContext object and are
commonly used for cross-cutting concerns like logging, tracing, and monitoring.
All hooks can be either synchronous or asynchronous. To make your hook
asynchronous, simply add the async keyword to the function.
Available Hooks
Zuplo provides several hooks for different stages of the request/response
pipeline:
Request Pipeline Hooks
addPreRoutingHook - Executes before route matching, can modify the
request URL or headers
addRequestHook - Executes after route matching but before handlers, can
return early responses
Response Pipeline Hooks
addResponseSendingHook - Executes before the response is sent and can
modify it
addResponseSendingFinalHook - Executes after all processing but cannot
modify the response
The addResponseSendingHook method adds a hook that fires just before the
response is sent to the client. This hook can modify the response by returning a
new Response object. Multiple hooks execute in the order they were added.
This example shows a tracing policy that ensures trace headers are consistent
between requests and responses:
import { ZuploContext, ZuploRequest } from "@zuplo/runtime";export async function tracingPlugin( request: ZuploRequest, context: ZuploContext, policyName: string,) { // Get the trace header let traceparent = request.headers.get("traceparent"); // If not set, add the header to the request if (!traceparent) { traceparent = crypto.randomUUID(); const headers = new Headers(request.headers); headers.set("traceparent", traceparent); return new ZuploRequest(request, { headers }); } context.addResponseSendingHook((response, latestRequest, context) => { // If the response doesn't have the trace header that matches, set it if (response.headers.get("traceparent") !== traceparent) { const headers = new Headers(response.headers); headers.set("traceparent", traceparent); return new Response(response.body, { headers, }); } return response; }); return request;}
ts
Hook: OnResponseSendingFinal
The addResponseSendingFinalHook method adds a hook that fires immediately
before the response is sent to the client. Unlike OnResponseSending, this hook
cannot modify the response - it's immutable at this point. This hook is ideal
for logging, analytics, and monitoring tasks.
This example logs the total request processing time:
import { ZuploContext, ZuploRequest } from "@zuplo/runtime";export default async function handler( request: ZuploRequest, context: ZuploContext,) { const start = Date.now(); context.addResponseSendingFinalHook(async (response, latestRequest) => { const end = Date.now(); const delta = end - start; context.log.debug(`Request took ${delta}ms`); }); return fetch(request);}
ts
Example: Asynchronous Analytics
This hook can block the response. To run asynchronous tasks without blocking,
use context.waitUntil() to ensure your async work completes after the response
is sent:
export default async function handler( request: ZuploRequest, context: ZuploContext,) { // Clone request before it's consumed by the handler const requestClone = request.clone(); context.addResponseSendingFinalHook(async (response, latestRequest) => { // Clone response to read the body without consuming it const responseClone = response.clone(); const asyncAnalytics = async () => { const requestBody = await requestClone.text(); const responseBody = await responseClone.text(); await fetch("https://analytics.example.com/log", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ requestId: context.requestId, requestBody, responseBody, status: response.status, timestamp: new Date().toISOString(), }), }); }; // Don't block the response context.waitUntil(asyncAnalytics()); }); return fetch(request);}
ts
Best Practices
1. Order Matters
Hooks execute in the order they're added. Plan your hook order carefully:
// First hook adds authentication infocontext.addResponseSendingHook((response) => { response.headers.set("X-User-ID", request.user?.sub || "anonymous"); return response;});// Second hook adds timing (sees the user header)context.addResponseSendingHook((response) => { response.headers.set("X-Processing-Time", `${Date.now() - start}ms`); return response;});
ts
2. Clone Before Reading
Always clone requests and responses before reading their bodies:
// ❌ Bad - consumes the original bodyconst body = await response.text();// ✅ Good - preserves the originalconst clone = response.clone();const body = await clone.text();