Zuplo

Route to Different Backends Based on Geolocation

This guide explains how to create a Zuplo policy that routes requests to different backend URLs based on the user's country.

Overview

When running a global API, you may want to route requests to region-specific backends for better performance, compliance, or data residency requirements. Zuplo makes this easy with built-in geolocation capabilities.

How It Works

Zuplo provides geolocation information for every request through the context.incomingRequestProperties object. This includes the country code, city, region, and other geographic details automatically determined from the request's IP address.

Creating a Geolocation Routing Policy

Create a new policy file in your project:

typescriptCode
// policies/geolocation-routing.ts import { ZuploContext, ZuploRequest } from "@zuplo/runtime"; export default async function policy( request: ZuploRequest, context: ZuploContext, ) { const country = context.incomingRequestProperties.country; // Route based on country switch (country) { case "US": context.custom.backendUrl = "https://us-east.example.com"; break; case "CA": context.custom.backendUrl = "https://ca-central.example.com"; break; case "GB": case "FR": case "DE": // Route European countries to EU backend context.custom.backendUrl = "https://eu-west.example.com"; break; case "JP": case "KR": // Route Asian countries to Asia-Pacific backend context.custom.backendUrl = "https://asia-pacific.example.com"; break; default: // Default backend for all other countries context.custom.backendUrl = "https://global.example.com"; } return request; }

Using Environment Variables

For better maintainability, store backend URLs in environment variables:

typescriptCode
// policies/geolocation-routing.ts import { ZuploContext, ZuploRequest, environment } from "@zuplo/runtime"; export default async function policy( request: ZuploRequest, context: ZuploContext, ) { const country = context.incomingRequestProperties.country; // Use environment variables for backend URLs switch (country) { case "US": context.custom.backendUrl = environment.US_BACKEND_URL; break; case "GB": case "FR": case "DE": context.custom.backendUrl = environment.EU_BACKEND_URL; break; default: context.custom.backendUrl = environment.DEFAULT_BACKEND_URL; } return request; }

Advanced: Using a Configuration Map

For more complex routing rules, use a configuration map:

typescriptCode
// policies/geolocation-routing.ts import { ZuploContext, ZuploRequest, environment } from "@zuplo/runtime"; // Define routing configuration const ROUTING_CONFIG: Record<string, string> = { // North America US: "https://us-east.example.com", CA: "https://ca-central.example.com", MX: "https://us-east.example.com", // Europe GB: "https://eu-west.example.com", FR: "https://eu-west.example.com", DE: "https://eu-central.example.com", IT: "https://eu-south.example.com", // Asia Pacific JP: "https://asia-northeast.example.com", KR: "https://asia-northeast.example.com", AU: "https://asia-southeast.example.com", SG: "https://asia-southeast.example.com", // Default fallback DEFAULT: "https://global.example.com", }; export default async function policy( request: ZuploRequest, context: ZuploContext, ) { const country = context.incomingRequestProperties.country || "DEFAULT"; // Look up the backend URL for this country const backendUrl = ROUTING_CONFIG[country] || ROUTING_CONFIG.DEFAULT; context.custom.backendUrl = backendUrl; // Optionally, add the country as a header for the backend request.headers.set("X-Client-Country", country); return request; }

Using Additional Location Data

Zuplo provides more than just country information. You can use other properties for more granular routing:

typescriptCode
// policies/advanced-geolocation-routing.ts import { ZuploContext, ZuploRequest } from "@zuplo/runtime"; export default async function policy( request: ZuploRequest, context: ZuploContext, ) { const geo = context.incomingRequestProperties; // Route based on continent for broader regions if (geo.continent === "NA") { context.custom.backendUrl = "https://americas.example.com"; } else if (geo.continent === "EU") { context.custom.backendUrl = "https://europe.example.com"; } else if (geo.continent === "AS") { context.custom.backendUrl = "https://asia.example.com"; } else { context.custom.backendUrl = "https://global.example.com"; } // Add detailed location headers for the backend request.headers.set("X-Client-Country", geo.country || ""); request.headers.set("X-Client-City", geo.city || ""); request.headers.set("X-Client-Region", geo.region || ""); request.headers.set("X-Client-Timezone", geo.timezone || ""); // Log detailed routing information context.log.info({ message: "Routing request based on location", country: geo.country, city: geo.city, region: geo.region, continent: geo.continent, coordinates: `${geo.latitude},${geo.longitude}`, backend: context.custom.backendUrl, }); return request; }

Using the Backend URL in a Handler

After the policy sets the backend URL in context.custom.backendUrl, you need a handler that uses this value.

Option 1: Custom Handler

Create a custom handler that reads the backend URL from context:

typescriptCode
// modules/geolocation-handler.ts import { ZuploContext, ZuploRequest } from "@zuplo/runtime"; export default async function handler( request: ZuploRequest, context: ZuploContext, ) { // Get the backend URL set by the geolocation policy const backendUrl = context.custom.backendUrl; if (!backendUrl) { return new Response("Backend URL not configured", { status: 500 }); } // Create the full URL by combining backend URL with the request path const url = new URL(request.url); const targetUrl = `${backendUrl}${url.pathname}${url.search}`; // Forward the request to the backend const response = await fetch(targetUrl, { method: request.method, headers: request.headers, body: request.body, }); return response; }

Option 2: Using URL Forward Handler

You can use Zuplo's built-in urlForwardHandler with a dynamic baseUrl that reads from context.custom:

jsonCode
{ "paths": { "/api/data": { "get": { "x-zuplo-route": { "corsPolicy": "anything-goes", "handler": { "export": "urlForwardHandler", "module": "$import(@zuplo/runtime)", "options": { "baseUrl": "${context.custom.backendUrl}" } }, "policies": { "inbound": ["geolocation-routing"] } } } } } }

This approach is the simplest - the urlForwardHandler will automatically forward requests to the backend URL set by your geolocation policy.

Adding the Policy to Your Route

Choose one of the handler options above and configure your route accordingly.

For Option 1 (Custom Handler):

jsonCode
{ "paths": { "/api/data": { "get": { "x-zuplo-route": { "corsPolicy": "anything-goes", "handler": { "export": "default", "module": "$import(./modules/geolocation-handler)" }, "policies": { "inbound": ["geolocation-routing"] } } } } } }

For Option 2 (URL Rewrite Handler), see the configuration shown above.

Testing Your Policy

1. Using a VPN

Test from different countries using a VPN service to verify the routing works correctly.

2. Adding Logging

Add comprehensive logging to debug the routing decisions:

typescriptCode
export default async function policy( request: ZuploRequest, context: ZuploContext, ) { const geo = context.incomingRequestProperties; const country = geo.country || "UNKNOWN"; const backendUrl = ROUTING_CONFIG[country] || ROUTING_CONFIG.DEFAULT; context.custom.backendUrl = backendUrl; // Log the routing decision with full context context.log.info({ requestId: context.requestId, country: country, city: geo.city, region: geo.region, asn: geo.asn, asOrganization: geo.asOrganization, backend: backendUrl, }); return request; }

3. Using Test Mode

In your development environment, you can create a test policy that simulates different locations:

typescriptCode
// policies/test-geolocation-routing.ts export default async function policy( request: ZuploRequest, context: ZuploContext, ) { // Check for test header const testCountry = request.headers.get("X-Test-Country"); if (testCountry && environment.NODE_ENV !== "production") { const backendUrl = ROUTING_CONFIG[testCountry] || ROUTING_CONFIG.DEFAULT; context.custom.backendUrl = backendUrl; context.log.warn(`TEST MODE: Routing as if from ${testCountry}`); return request; } // Fall back to real geolocation const country = context.incomingRequestProperties.country || "DEFAULT"; context.custom.backendUrl = ROUTING_CONFIG[country] || ROUTING_CONFIG.DEFAULT; return request; }

Considerations

Performance

The geolocation data is determined at the edge, so there's no additional latency for IP lookups. All location properties are immediately available in the context.

Accuracy

Geolocation based on IP addresses is generally accurate but may occasionally misidentify locations, especially for:

  • VPN users
  • Corporate networks with centralized egress
  • Mobile networks that may route through different regions
  • Proxy servers

Compliance

When routing based on geolocation for compliance reasons, consider:

  • GDPR requirements for EU countries
  • Data residency laws in specific countries
  • Adding additional verification for sensitive operations
  • Documenting your geolocation-based routing for compliance audits

Fallback Strategy

Always implement a default fallback to ensure requests are handled even when:

  • Country information is unavailable
  • A new country code appears that isn't in your configuration
  • The geolocation service has issues

Next Steps

Last modified on