Every API gateway eventually makes you write custom code. Maybe a partner’s sending a weird header, maybe you need to enrich requests with the caller’s org, maybe a backend field shouldn’t be leaking to consumers. The code itself is small, twenty or thirty lines you’ve written a hundred times. The annoying part is everything else: the language, the file layout, the deploy cycle, and the shape the code has to take to run.
With Kong, you write Lua. With AWS API Gateway, you wrestle with Velocity Template Language or wire up Lambda authorizers. Over in Azure APIM world, you write C# snippets inside XML policy documents. None of these are languages most backend developers reach for voluntarily (but I’ll admit it might get me dusting off my copy of Sams’ Teach Yourself C# in 24 Hours book. Remember them? Anyone…).
Zuplo takes a different approach: custom policies are TypeScript functions that
use the standard web Request and Response objects. If you’ve written a
service worker or an edge function before, you’re already pretty familiar with
this programming model. This tutorial walks through a custom inbound policy, a
custom outbound policy, wiring them up, and deploying, in about 15 minutes.
- You want custom logic in your API gateway without learning Lua, VTL, or inline C#
- You're evaluating Zuplo and want to see what writing a real policy looks like
- You're migrating from Kong, AWS API Gateway, or Azure APIM and comparing developer experience
What Are Zuplo Policies?
Every request to a Zuplo gateway flows through a pipeline:
- Inbound policies run before the request handler. They can inspect,
validate, or modify the incoming request, or short-circuit the pipeline by
returning a
Response. - The request handler produces the response. Typically a URL forward to your backend, but it can also be a custom handler.
- Outbound policies run after the handler. They can inspect or transform the response before it’s sent back to the client.
You can attach multiple inbound and outbound policies to each route, and they execute in order. Mix built-in policies (like rate limiting or API key auth) with your own custom logic.
Policy Fundamentals
The full picture of how policies, handlers, and the request pipeline fit together in Zuplo.
Prerequisites
You’ll need a Zuplo project. If you don’t have one:
- Local development: Run
npx create-zuplo-api@latest --emptythennpm run devin the new directory. See the local development guide. - Portal development: Sign in at portal.zuplo.com and create a new empty project. See Step 1: Setup a Basic Gateway.
Either way, you’ll end up with a project with a config/ directory for route
and policy configuration and a modules/ directory for custom TypeScript code.
Writing a Custom Inbound Policy
A practical scenario: your API requires a custom X-Request-Source header on
every POST request. If it’s missing or invalid, reject the request with a
400 Bad Request before it reaches your backend.
The Policy Function
Create modules/validate-source-header.ts:
The key concepts:
- Function signature: An inbound policy receives a
ZuploRequest(which extends the standard webRequestwith properties likeuser,params, andquery), aZuploContextfor logging and metadata,optionsfrom the policy configuration (typedneverhere since this policy takes none), and thepolicyName. - Return
requestto continue: Return the request object and Zuplo passes it to the next policy in the chain, or to the handler if this is the last. - Return a
Responseto short-circuit: Return aResponsedirectly and Zuplo sends it back to the client. The handler and outbound policies never execute. - Standard web APIs: Nothing Zuplo-specific about
ResponseorJSON.stringify. If you’ve usedfetchor built a Cloudflare Worker, you already know these APIs.
Register the Policy
Add the policy to config/policies.json. The $import(...) syntax is Zuplo’s
module reference: it resolves a file path (no .ts extension) or an npm package
name at build time and wires the exported function into the policy. No options
block is needed here.
Then attach it to a route in config/routes.oas.json. This is a standard
OpenAPI document with Zuplo extensions under x-zuplo-route, where you declare
the handler (here, the built-in urlForwardHandler that proxies to your
backend) and the policy chain:
A POST to /items without the header now returns:
Writing a Custom Outbound Policy
Now the response side. Say your backend returns user objects with an internal
internalId field that should never reach API consumers. You also want to add
an X-Request-Id header so consumers can reference a specific request when
reporting issues.
The Policy Function
Create modules/clean-response.ts:
Key differences from inbound policies:
- First parameter is
Response: Outbound policies receive the handler’s response, plus the original request, context, options, and policy name. - Must return a
Response: Return the original response or construct a new one. - Only runs on successful responses: By default, custom outbound policies
execute when
response.ok === true(status codes 200 to 299). A 500 from your backend skips the outbound policy. - Copy headers, then adjust: Clone
response.headersinto a newHeadersobject so you keep CORS and anything Zuplo or the backend set, then layer your own headers on top.
Common mistake:
Forgetting to copy response.headers into the new Response is the most
common outbound policy bug. The response still works, but CORS headers and
custom headers from earlier policies silently disappear, so your frontend
starts failing preflight checks for no obvious reason. A close second is
forwarding the original content-length after changing the body, which can
cause the edge to truncate or reject the response.
Register the Outbound Policy
Add the outbound policy to config/policies.json:
And attach it to the route’s outbound array:
Your API now strips internalId from every JSON response and adds a request ID,
without changing a line of backend code.
Making Policies Configurable with Options
Hard-coding field names works, but different routes often need to strip
different fields. Zuplo policies support an options object you can configure
per policy instance.
A more flexible version of the outbound policy:
Configure which fields to strip per route in policies.json:
Whatever JSON you declare under options in policies.json is exactly what
gets passed into your handler’s options argument at runtime, so the shape is
entirely up to you. Declare a matching TypeScript type on that argument (like
CleanResponseOptions above) and TypeScript will autocomplete and typecheck
every access, so a typo in options.fieldsToRemove fails at build time instead
of silently doing nothing in production.
Custom Code Patterns
More examples for structuring custom inbound and outbound policies, including options validation and shared helpers.
How This Compares to Other Gateways
You saw the whole Zuplo version in the sections above: one TypeScript file, a bit of JSON wiring, done. Compare that to the same header-validation job on a Kong plugin, which needs two Lua files, a priority/version table, and a restart or admin API call to deploy:
On AWS API Gateway, the same task means a Lambda authorizer that returns an IAM
policy document, plus IAM roles, cold starts, and a Gateway Response template to
stop AWS collapsing your 400 into a generic 401. On Azure APIM, it’s inline C#
smuggled inside an XML <choose><when> block, debugged without real IDE
support.
The Zuplo advantage isn’t TypeScript by itself. It’s that policies use the same
Request and Response objects you’d use in any modern JavaScript runtime,
with no gateway-specific SDK to learn.
Deploy and Test
With your policies wired up, deploying is straightforward:
- Local development: Your gateway is already running via
npm run dev. Module changes hot-reload. - Portal: Click Save. The working copy updates immediately and you can test in the built-in API tester.
- Production: Push to your connected Git repository. Zuplo builds and deploys to 300+ edge locations automatically.
Test with curl:
If you’d rather stay in the browser, the Zuplo Portal has a Test Route button on any route inside your project. It opens a built-in tester where you can set the method, headers, and body and fire a request without touching a terminal.
Other Common Custom Policy Patterns
A few more custom policies you’ll see in production:
- Request enrichment: Look up the authenticated user’s organization in a database and add it as a header before forwarding.
- Response caching logic: Check a custom cache header from your backend and
set
Cache-Controldynamically. - Audit logging: Clone the request, extract key fields, and send them to
your logging service via
context.waitUntilto avoid latency. - Request body transformation: Parse an incoming JSON body, reshape it to
your backend’s expected format, and return a new
ZuploRequest.
Where to Go From Here
Most gateways push custom logic into a dialect and a deployment loop that
doesn’t match how you write the rest of your code. Zuplo keeps it in TypeScript,
against the same Request and Response objects you’d reach for anywhere else,
so a custom policy stops being a special artefact and starts being a function
you can read, test, and review like everything else in your codebase.
If you’re migrating from another gateway, our migration guides for Kong, AWS API Gateway, and Azure APIM include side-by-side policy and concept mappings.
Custom Code Policy Reference
Full type signatures and examples for both custom-code-inbound and custom-code-outbound policies, including how options, context.waitUntil, and context.log work.
