Akamai

Routing a Test Hostname to a Separate Backend

Applies to
Gatewayruntime for API, AI, and MCP gateways
Hosting
Managed EdgeNot availableManaged DedicatedSelf-HostedNot available

Test a new backend release against production data by sending a second public hostname through the same production Zuplo gateway. Akamai tags each request with a header based on the hostname, and one Zuplo policy uses that header to pick the production or test backend. Your frontend and API clients change nothing except the hostname they call.

How it works

Akamai property
Backends
api.example.comapi-test.example.com
Production backend
Test backend
Production clients
Test clients
Production Zuplo gateway

Akamai turns the hostname into a header, and the gateway turns the header into a backend. Everything between the client and the backend is the production path for both hostnames: the same Akamai property, the same gateway deployment, and the same routes, policies, and rate limits.

The examples on this page use these hostnames. Replace them with your own.

Example hostnamePurpose
api.example.comPublic production hostname
api-test.example.comNew public test hostname, on the same Akamai property
gateway.example.comProduction Zuplo custom domain, the Akamai origin
origin-api.example.comProduction backend
origin-api-test.example.comTest backend that runs the release candidate

Prerequisites

  • An Akamai property that already forwards api.example.com to your production Zuplo gateway. See Setting up Akamai CDNs.
  • A test backend that runs the release you want to validate and can reach production data.
  • An edge certificate and DNS access for the new test hostname.

Step 1: Set the routing header in Akamai

Both public hostnames live on the same Akamai property and use the same origin: the production Zuplo custom domain. Akamai always sets x-zuplo-route, so it overwrites any value a client sends.

In Property Manager, edit the property and make these changes:

  1. Default rule. Add a Modify Outgoing Request Header behavior. Set the action to Add or Modify, the custom header name to x-zuplo-route, and the value to prod.
  2. New child rule named Test routing. Add a Hostname match for api-test.example.com. Add a Modify Outgoing Request Header behavior with the custom header name x-zuplo-route and the value test. Because the child rule runs after the default rule, it wins for the test hostname.
  3. Cache key. Make sure test and production responses can't share a cache entry. Set Cache Key Hostname to Incoming Host Header, or turn off caching in the Test routing rule.
  4. Hostname. Add api-test.example.com to the property's hostnames with its edge certificate, and create its CNAME record in your DNS.

Activate the property version. Clients that test the release only change the base URL they call. They never send the header themselves.

Step 2: Add the backend URLs in Zuplo

In the Zuplo Portal, open your project and select Settings → Environment Variables. Add these variables to the production environment. If you already have a variable for the production backend, keep its name and use it in place of BASE_URL_PROD in the following steps.

TerminalCode
BASE_URL_PROD=https://origin-api.example.com BASE_URL_TEST=https://origin-api-test.example.com

You don't need a new Zuplo environment, branch, or custom domain. The production deployment you run today serves both hostnames. For more on variables, see Environment variables.

Step 3: Add the routing policy

This policy follows the pattern in Canary routing for employees, adapted to one signal: the x-zuplo-route header that Akamai sets. Any value other than test goes to production, so a missing or unexpected value never reaches the test backend. You add the policy once; it doesn't change between releases.

Create modules/test-routing.ts:

TypeScriptmodules/test-routing.ts
import { HttpProblems, ZuploContext, ZuploRequest, environment, } from "@zuplo/runtime"; // Set by the Akamai property on every request. Never set by clients. const ROUTE_HEADER = "x-zuplo-route"; export default async function testRouting( request: ZuploRequest, context: ZuploContext, ) { const isTest = request.headers.get(ROUTE_HEADER)?.trim().toLowerCase() === "test"; const backendUrl = isTest ? environment.BASE_URL_TEST : environment.BASE_URL_PROD; if (!backendUrl) { context.log.error("Backend base URL is not configured", { stage: isTest ? "test" : "prod", }); return HttpProblems.internalServerError(request, context); } // The route's URL Forward handler reads this value as its baseUrl. context.custom.backendUrl = backendUrl; if (isTest) { context.log.info("Routing to test backend", { path: new URL(request.url).pathname, }); } // Lets testers confirm which backend answered. const stage = isTest ? "test" : "prod"; context.addResponseSendingHook((response) => { const tagged = new Response(response.body, response); tagged.headers.set("x-backend-stage", stage); return tagged; }); return request; }

Register it in config/policies.json:

JSONconfig/policies.json
{ "name": "test-routing", "policyType": "custom-code-inbound", "handler": { "export": "default", "module": "$import(./modules/test-routing)" } }

Step 4: Point each route at the chosen backend

On every route that forwards to the backend, make two changes:

  1. Set the URL Forward handler baseUrl to ${context.custom.backendUrl} instead of the environment variable.
  2. Add test-routing to the inbound policies. Keep your existing policies. The routing policy only reads a header, so its position in the list doesn't matter.
JSONconfig/routes.oas.json
"/v1/products/{id}": { "get": { "x-zuplo-route": { "handler": { "export": "urlForwardHandler", "module": "$import(@zuplo/runtime)", "options": { "baseUrl": "${context.custom.backendUrl}" } }, "policies": { "inbound": ["<your-existing-policies>", "test-routing"] } } } }

If the route connects to your backend with mTLS, keep the mtlsCertificate option you use today and configure the test backend to trust the same client certificate. One setting then covers both backends.

Deploy this change to production once. From then on, every route, policy, and rate limit is identical for both hostnames; only the backend differs.

Test the setup

Send these requests and check the x-backend-stage response header on each:

RequestExpected x-backend-stage
curl -i https://api.example.com/v1/products/1prod
curl -i https://api-test.example.com/v1/products/1test
curl -i https://api.example.com/v1/products/1 -H "x-zuplo-route: test"prod (Akamai overwrote the header)

If the third request returns test, the default rule from Step 1 isn't setting the header. Fix that before you share the test hostname.

Release flow

A backend release doesn't require a gateway change:

  1. Your CI/CD pipeline deploys the new backend version to the test backend.
  2. Testers and test builds of your frontend call api-test.example.com. Their requests go through the production Akamai property and the production gateway and its policies, to production data.
  3. When testing passes, promote the same build to the production backend.

Keep using your non-production Zuplo environments to test gateway changes. Changing the gateway and the backend in the same test makes a failure hard to place. Serving both hostnames from one gateway also means the test and production paths can't drift apart, which a separate Zuplo environment for testing would allow.

Guardrails

  • Only Akamai sets the header. If gateway.example.com is reachable without going through Akamai, anyone could send x-zuplo-route: test. Restrict the gateway to Akamai traffic with a shared secret header or mTLS. See Securing Zuplo from direct access.
  • The test hostname uses production data. Treat it as production: keep the same authentication and rate limits, and don't publish the hostname.
Last modified on