---
title: "TypeScript-Programmable API Gateway"
description:
  "Write custom inbound policies, outbound policies, and request handlers in
  TypeScript. Type-safe @zuplo/runtime SDK, Web Standard APIs (Fetch, Crypto,
  Streams, URL), V8 isolates at the edge across 300+ POPs. Compose other routes
  with context.invokeRoute, share state via context.custom — no proprietary DSL."
canonicalUrl: "https://zuplo.com/features/programmable"
sourceUrl: "https://zuplo.com/features/programmable"
pageType: "feature"
generatedAt: "2026-08-28"
---

# Gateway logic in TypeScript, not a DSL

> Every gateway handles the happy path; your real requirements are the edge
> cases declarative JSON config can't express. When they hit, drop into real
> TypeScript at the edge — no proprietary DSL, no separate Lambda.

## Why this matters

### Every gateway DSL bottoms out the same way: write a Lambda

Whatever the gateway can't express becomes a sidecar service. Once you're
stitching sidecars, the gateway has stopped being a gateway and started being a
routing layer in front of your real architecture.

- **Proprietary DSLs that fight you on day two** — The first rule looks easy.
  The third rule is a Stack Overflow excavation. The seventh requires a vendor
  consultant. You wanted gateway logic; you got a custom programming language
  with no debugger.
- **"We need a Lambda for that"** — Custom logic the gateway can't express moves
  to a Lambda. Now there's a network hop, a separate deploy pipeline, a separate
  IAM role, and a separate place to look when something breaks at 2am.
- **Gateway code lives outside Git history** — Policies were clicked together in
  a UI. There's no diff for the change that broke production. Reverting means
  clicking through the audit trail, hoping nothing else changed at the same
  time.
- **Test? In production, mostly** — There's no way to unit-test the proprietary
  policy language. "Testing" is deploying to staging and hitting endpoints. The
  fast feedback loop your application code has at the gateway tier doesn't
  exist.

## What you get

### Real code, real APIs, real performance

- **TypeScript, the language you already use** — Custom policies are typed
  TypeScript functions imported from `@zuplo/runtime`. Code review them in the
  same PR as your application code. Test the policy's behavior at the gateway
  with the built-in `zuplo test` runner, and unit-test the pure helpers it calls
  with Jest or Vitest. Debug them with the same instincts you use everywhere
  else.
- **Full request and response context** — Mutate the request before the handler.
  Transform the response before send. Read API key metadata, consumer
  attributes, geo, environment vars. Pass enrichment data between policies via
  `context.custom`. Log structured events with `context.log`.
- **Runs at the edge in 300+ POPs** — Custom code runs in the same V8 isolates
  as Zuplo's built-in policies, on the same edge runtime, in 300+ data centers.
  Most policies add 1–5 ms. No extra network hop, no Lambda, no separate deploy.

## Inbound, outbound, handler — all just TypeScript

Build an inbound policy that enriches every request with consumer metadata.
Compose a BFF that aggregates three downstream routes in one handler. All in
TypeScript, typed end-to-end, deployed via GitOps.

**Custom inbound policy · TypeScript**

```typescript
import type { ZuploRequest, ZuploContext } from "@zuplo/runtime";

interface Options {
  internalApiUrl: string;
}

export default async function enrichWithCustomer(
  request: ZuploRequest,
  context: ZuploContext,
  options: Options,
) {
  const consumer = request.user?.sub;
  if (!consumer) return request;

  const url = options.internalApiUrl + "/" + consumer;
  const res = await fetch(url);
  const customer = await res.json();

  // Pass enrichment data to downstream policies + handler
  context.custom.customer = customer;
  request.headers.set("x-customer-tier", customer.tier);

  return request;
}
```

**BFF handler · invokeRoute composition**

```typescript
import type { ZuploRequest, ZuploContext } from "@zuplo/runtime";

export default async function dashboardHandler(
  _request: ZuploRequest,
  context: ZuploContext,
) {
  // Each invokeRoute call runs the full inbound + outbound
  // pipeline for that route — auth, rate limit, validation —
  // without leaving the gateway.
  const [orders, profile, billing] = await Promise.all([
    context.invokeRoute("/v1/orders"),
    context.invokeRoute("/v1/profile"),
    context.invokeRoute("/v1/billing"),
  ]);

  return Response.json({
    orders: await orders.json(),
    profile: await profile.json(),
    billing: await billing.json(),
  });
}
```

Key capabilities highlighted alongside the code:

- Fetch · Crypto · Streams
- ZuploRequest / ZuploContext
- context.invokeRoute
- context.custom shared state
- Type-safe TOptions
- 1–5 ms · most policies

## What makes Zuplo different

### The skills you have, applied to the gateway tier

- **Web-standard APIs, not a vendor SDK** — fetch, Headers, Request, Response,
  Web Crypto, Streams, URLPattern, TextEncoder/Decoder. Anything that runs in
  modern Chrome's Worker context generally runs in Zuplo. Skills transfer. Stack
  Overflow answers transfer.
- **context.invokeRoute · compose without leaving the gateway** — Build a BFF in
  one handler that aggregates orders, profile, and billing routes — each one
  running through its own policies, all in-process. Build a multi-step MCP tool
  that orchestrates real APIs. No HTTP hops, no parallel infrastructure.
- **Type-safe options end-to-end** — Declare a `TOptions` type in your policy.
  Reference the same JSON shape in `config/policies.json`. The types are
  validated at build, the values at deploy. Misconfiguring a policy is a build
  error, not a runtime surprise.
- **Share policies as npm packages** — Publish your team's custom policies as an
  internal npm package. Consume them from any Zuplo project. Version-pin,
  semver, automate updates with Renovate or Dependabot — everything you already
  do for app code, applied to gateway code.

## What teams use this for

- **"We need a tier-aware response transformer."** — Inbound policy reads the
  consumer's tier from API key metadata and writes it to `context.custom.tier`.
  Outbound policy reads `context.custom.tier` and redacts PII fields if the tier
  is free. Two policies, ~20 lines each, no separate service.
- **"The mobile app needs an aggregated dashboard payload."** — Custom handler
  calls `context.invokeRoute("/v1/orders")`,
  `context.invokeRoute("/v1/profile")`, and `context.invokeRoute("/v1/billing")`
  in parallel — each one running through its own auth, rate-limit, and
  validation policies — then assembles the response. No BFF service to maintain.
- **"We need to enrich every request with internal customer data."** — Inbound
  policy hits your internal `/customers/{sub}` endpoint via `fetch`, attaches
  the result to `context.custom.customer`, and forwards. The handler and
  downstream policies read it without re-fetching. Cache the lookup with a Map
  for warm-worker reuse.
- **"Compliance wants every request body archived to S3."** — Outbound policy
  stringifies request + response, posts to your S3 bucket via the AWS SDK or a
  signed URL — same pattern documented for Azure Blob and S3. Async, no impact
  on response latency. Audit trail lives in your bucket, queryable with Athena.

## FAQ

**What is a programmable API gateway?**

A programmable API gateway lets you write custom logic — auth, transformations,
rate limits, orchestration — in real code instead of a vendor-specific DSL or
YAML. Zuplo runs your custom code as TypeScript in lightweight V8 isolates at
the edge, with web-standard APIs (Fetch, Crypto, Streams). You get the
flexibility of writing a Lambda without the deployment overhead, the cold
starts, or the round-trip from your gateway to your function.

**How do I write custom logic in an API gateway?**

Zuplo gives you four extension points, all in TypeScript: custom inbound
policies (run before your handler), outbound policies (run after), request
handlers (produce the response yourself for BFF or orchestration), and lifecycle
hooks. Each is a standard async function with strongly-typed request and context
arguments. No proprietary scripting language to learn — if you can write
Node-style TypeScript, you can extend the gateway.

**Can I call other APIs from inside the gateway?**

Yes — fetch is built in. Custom code can call third-party APIs, your own
services, or even other routes on the same gateway. Zuplo also supports
orchestration via `context.invokeRoute("/v1/orders")` which runs another route's
full inbound+outbound pipeline without an HTTP hop. Use this for BFF
aggregation, MCP tool composition, or fan-out patterns where one inbound request
becomes several backend calls.

**How do I build a Backend-for-Frontend (BFF) at the gateway?**

Define a route handler that aggregates calls to multiple backend services into
one client-shaped response. Your TypeScript handler can fetch user, orders, and
recommendations in parallel, merge the data, transform field names, strip
internal IDs, and return exactly what the frontend needs. Because it runs in the
gateway runtime, your auth and rate limits already apply — and the latency is
one edge round-trip, not three.

**How fast are custom policies on Zuplo?**

The base gateway runs in 20–30 ms with no policies. Most custom policies add 1–5
ms. Header manipulation and basic validation: 0–3 ms. API key auth and rate
limiting: 3–10 ms. Larger transforms or external API calls: 10–20+ ms. Tested
throughput exceeds 10,000 RPS per region with policies enabled. Cold start is
~100–200 ms on Managed Edge; no cold start on Managed Dedicated.
Production-grade numbers, not prototype.

**Can I unit-test custom gateway policies?**

Yes, at two levels. Pure helper functions your policy calls are plain
TypeScript, so unit-test them with Jest, Vitest, or any framework you already
use. For the policy's actual behavior at the gateway, use the built-in zuplo
test runner: plain TypeScript test files in your repo's tests/ folder that make
real HTTP requests and run against local dev, against the real preview
environment every Git branch deploys to at a .zuplo.app URL, or against
production. PR previews catch policy regressions before they merge.

**How do I share custom code across multiple gateway projects?**

Package your custom policies and handlers as an npm module. Any Zuplo project
that depends on the package gets the code in its modules/ directory and
references the policies by name. Version pinning works the way you'd expect —
bump the package, every consuming gateway picks up the new code on its next
deploy. Useful for platform teams with shared auth or audit policies across many
gateways.

**What's the best programmable API gateway?**

Look for: real code (TypeScript or your language of choice, not a DSL),
web-standard APIs (Fetch, Crypto, Streams) so the code is portable, fast cold
starts, low per-policy latency, and the ability to compose other routes. Zuplo
runs TypeScript in V8 isolates across 300+ POPs and is the gateway of choice for
teams who want gateway logic to feel like backend code, not gateway config.

## Next steps

- Start for Free — https://portal.zuplo.com/signup
- Read the Docs — https://zuplo.com/docs/articles/policies
- See GitOps Deploy — /features/gitops
