Guides

Transform Route Parameters for URL Rewrite

This guide explains how to transform incoming route parameter values in an inbound policy before the URL Rewrite handler uses them to build the upstream URL. This pattern is useful when your public API paths use different naming conventions than your internal backend.

Overview

When you use the URL Rewrite handler, it builds the upstream URL by interpolating values like ${params.resourceType} directly from the incoming route parameters. Sometimes, however, you need to change those values before the rewrite happens. Common scenarios include:

  • Value mapping — translating a public-facing parameter like order to an internal value like customerorder
  • Case normalization — converting Products to products before forwarding
  • Path translation — mapping user-friendly slugs to internal identifiers

One approach is to read the route parameters in an inbound policy, transform them, store the results on context.custom, and reference the transformed values in the URL Rewrite pattern. This keeps the original route parameters intact while exposing the transformed values under clearly named keys. If you prefer, you can instead modify params directly on the request — see Alternative: Modify Route Parameters Directly.

Step-by-Step Example

The solution has three parts: an inbound policy that reads request.params and stores transformed values on context.custom, a URL Rewrite handler that references those values using ${context.custom.*} in the rewritePattern, and route configuration that wires the two together.

Imagine your public API exposes a route like /api/:resourceType/:resourceId, but your backend expects the resource type to be prefixed with customer. A request to /api/order/123 should be forwarded to https://backend.example.com/api/customerorder/123.

1. Write the Inbound Policy

Create a custom inbound policy that reads the route parameters, transforms the values, and stores them on context.custom:

TypeScriptmodules/transform-params.ts
import { ZuploContext, ZuploRequest } from "@zuplo/runtime"; export default async function ( request: ZuploRequest, context: ZuploContext, options: Record<string, unknown>, policyName: string, ): Promise<ZuploRequest | Response> { // Read the original route parameter const resourceType = request.params.resourceType; // Transform the value — prefix with "customer" const transformedResourceType = `customer${resourceType}`; // Store the transformed value on context.custom context.custom.transformedResourceType = transformedResourceType; context.log.info({ message: "Transformed route parameter", original: resourceType, transformed: transformedResourceType, }); return request; }

2. Register the Policy

Add the policy to config/policies.json:

JSONconfig/policies.json
{ "policies": [ { "name": "transform-params", "policyType": "custom-code-inbound", "handler": { "export": "default", "module": "$import(./modules/transform-params)", "options": {} } } ] }

3. Configure the Route

Define the route in config/routes.oas.json with the inbound policy and a URL Rewrite handler that references context.custom:

JSONconfig/routes.oas.json
{ "paths": { "/api/{resourceType}/{resourceId}": { "x-zuplo-path": { "pathMode": "open-api" }, "get": { "summary": "Get resource by type and ID", "x-zuplo-route": { "corsPolicy": "none", "handler": { "export": "urlRewriteHandler", "module": "$import(@zuplo/runtime)", "options": { "rewritePattern": "https://backend.example.com/api/${context.custom.transformedResourceType}/${params.resourceId}" } }, "policies": { "inbound": ["transform-params"] } } } } } }

With this configuration, a request to /api/order/123 flows through the pipeline as follows:

  1. The route matches with params.resourceType = "order" and params.resourceId = "123"
  2. The transform-params inbound policy runs and sets context.custom.transformedResourceType = "customerorder"
  3. The URL Rewrite handler builds the upstream URL: https://backend.example.com/api/customerorder/123

Alternative: Modify Route Parameters Directly

Instead of storing transformed values on context.custom, you can return a new ZuploRequest with modified params. The URL Rewrite handler reads ${params.*} from the request it receives, so any parameters you set on the returned request flow through to the rewritePattern:

TypeScriptmodules/transform-params-direct.ts
import { ZuploContext, ZuploRequest } from "@zuplo/runtime"; export default async function ( request: ZuploRequest, context: ZuploContext, options: Record<string, unknown>, policyName: string, ): Promise<ZuploRequest | Response> { return new ZuploRequest(request, { params: { ...request.params, resourceType: `customer${request.params.resourceType}`, }, }); }

With this policy in front of a URL Rewrite handler whose rewritePattern is https://backend.example.com/api/${params.resourceType}/${params.resourceId}, the handler forwards a request for /api/order/123 to https://backend.example.com/api/customerorder/123.

Both approaches work. Modify params directly when you simply want the rewritten URL to use the transformed values. Use context.custom when you want to keep the original route parameters available (for logging or for use elsewhere in the pipeline) while passing transformed values alongside them.

Variations

Using a Lookup Map

For more complex mappings where the transformation is not a simple string operation, use a lookup object:

TypeScriptmodules/transform-params-map.ts
import { ZuploContext, ZuploRequest, HttpProblems } from "@zuplo/runtime"; // Map public resource types to internal names const RESOURCE_TYPE_MAP: Record<string, string> = { order: "customerorder", invoice: "billing-invoice", profile: "user-profile", subscription: "recurring-plan", }; export default async function ( request: ZuploRequest, context: ZuploContext, options: Record<string, unknown>, policyName: string, ): Promise<ZuploRequest | Response> { const resourceType = request.params.resourceType; const mappedType = RESOURCE_TYPE_MAP[resourceType]; if (!mappedType) { return HttpProblems.notFound(request, context, { detail: `Unknown resource type: ${resourceType}`, }); } context.custom.transformedResourceType = mappedType; return request; }

Transforming Multiple Parameters

You can transform any number of route parameters and store each on context.custom. Reference them individually in the rewrite pattern:

TypeScriptmodules/transform-multiple-params.ts
import { ZuploContext, ZuploRequest } from "@zuplo/runtime"; export default async function ( request: ZuploRequest, context: ZuploContext, options: Record<string, unknown>, policyName: string, ): Promise<ZuploRequest | Response> { // Normalize casing context.custom.version = request.params.version?.toLowerCase(); // Map resource type context.custom.resource = request.params.resource === "users" ? "customers" : request.params.resource; return request; }

Then use both values in the rewrite pattern:

JSONCode
{ "rewritePattern": "https://backend.example.com/${context.custom.version}/${context.custom.resource}/${params.id}" }

Combining with Body Transformation

If your API also needs to transform values in the request body alongside route parameters, you can handle both in the same inbound policy. Create a new ZuploRequest with a modified body while storing the route parameter transformations on context.custom:

TypeScriptmodules/transform-params-and-body.ts
import { ZuploContext, ZuploRequest } from "@zuplo/runtime"; export default async function ( request: ZuploRequest, context: ZuploContext, options: Record<string, unknown>, policyName: string, ): Promise<ZuploRequest | Response> { // Transform route parameter context.custom.transformedResourceType = `customer${request.params.resourceType}`; // Transform the request body if present if (request.headers.get("content-type")?.includes("application/json")) { const body = await request.json(); // Map fields in the body to match the backend schema const transformedBody = { ...body, type: context.custom.transformedResourceType, }; // Return a new request with the modified body return new ZuploRequest(request, { body: JSON.stringify(transformedBody), }); } return request; }

Best Practices

  • Use descriptive keys on context.custom — names like context.custom.transformedResourceType are easier to debug than generic keys like context.custom.value
  • Log transformations — use context.log to record original and transformed values so you can trace issues in production
  • Validate before transforming — return an appropriate error response (using HttpProblems) if a parameter value is unexpected, rather than forwarding bad data to your backend
  • Keep the policy focused — if your transformation logic is complex, consider splitting it into a separate utility module imported by the policy

Next Steps

  • URL Rewrite Handler — full reference for rewrite patterns and available interpolation variables
  • Custom Code Patterns — common patterns for writing inbound policies, outbound policies, and handlers
  • ZuploContext — reference for context.custom and other context properties
  • ZuploRequest — reference for request.params and constructing new requests
  • User-Based Backend Routing — a related pattern using context.custom with URL Rewrite for routing by user identity
Last modified on