Routing Is the Gateway’s Core Job
Request routing is the process of deciding where an incoming HTTP request should go. An API gateway receives a request (for example, GET https://api.example.com/users/123), matches it against a set of routing rules, and forwards it to the correct backend (also called an upstream), such as a monolith, a microservice, or a serverless function.
A route rule typically matches on one or more of these attributes:
- Host (domain):
api.example.comvsadmin.example.com - Path:
/users/123,/orders/987 - HTTP method:
GET,POST,PUT,DELETE - Other constraints (optional): headers, query params, client IP, etc. (not required for basic routing)
Once a route matches, the gateway selects an upstream target (one instance or one function), optionally rewrites the URL, and proxies the request.
Route Matching: Host-Based and Path-Based
Host-based routing
Host-based routing uses the request’s Host header (or SNI for TLS) to choose a backend. This is common when you want different subdomains to map to different systems.
api.example.com→ public API gateway routesadmin.example.com→ admin backendpartner.example.com→ partner API backend
Host-based routing is often the first split because it cleanly separates traffic domains and can reduce accidental overlap between routes.
Continue in our app.
You can listen to the audiobook with the screen off, receive a free certificate for this course, and also have access to 5,000 other free online courses.
Or continue reading below...Download the app
Path-based routing
Path-based routing matches the URL path (everything after the domain). Gateways usually support patterns such as:
- Exact match:
/users - Prefix match:
/users/matches/users/123,/users/profile - Wildcard/glob:
/api/* - Path parameters:
/users/{id}or/users/:id
In practice, prefix matching is the most common for service routing (for example, everything under /users/ goes to the user service).
HTTP Method Constraints (GET vs POST, etc.)
Two routes can share the same path pattern but differ by HTTP method. Method constraints help you:
- Send reads and writes to different backends (rare, but possible).
- Apply different policies per method (for example, stricter limits on
POST). - Avoid accidental matches (for example, only allow
GETon a read-only endpoint).
Example: same path, different method handling:
GET /orders/{id}→ order service (read handler)DELETE /orders/{id}→ order service (admin-only handler)
If a request matches the host and path but not the method, the gateway should treat it as “no route” (often returning 404 or 405 Method Not Allowed, depending on gateway behavior and configuration).
Priority and Ordering Rules (Why Some Routes “Win”)
When multiple routes could match the same request, the gateway needs a deterministic way to choose one. Most gateways use a combination of:
- Specificity: exact paths beat prefixes; longer prefixes beat shorter prefixes.
- Explicit priority: a numeric priority you set (higher wins).
- Declaration order: first match wins (common in simpler systems).
Consider these routes:
- Route A:
/users/(prefix) - Route B:
/users/admin/(prefix)
A request to /users/admin/settings should match Route B, because it is more specific (longer prefix). If your gateway uses “first match wins,” you must place Route B before Route A, or assign Route B a higher priority.
Practical rule: define the most specific routes first (or with highest priority), then broader catch-all routes last (like /api/*).
Mapping Public Endpoints to Upstream Services
Routing is not only about matching; it’s also about mapping. The gateway often exposes a stable public API surface while your internal services can have different hostnames, ports, and path structures.
Common routing patterns
- Monolith catch-all:
/api/*→ monolith upstream - Service split:
/users/*→ user service,/orders/*→ order service - Serverless:
/webhooks/*→ function per event type
Example target mapping (conceptual):
Public: https://api.example.com/users/* -> Upstream: http://users-service:8080/* (cluster internal DNS)Public: https://api.example.com/orders/* -> Upstream: http://orders-service:8080/*Public: https://api.example.com/api/* -> Upstream: http://monolith:8080/*Step-by-Step: Build a Simple Route Table
This step-by-step approach helps you design routes without surprises.
Step 1: List your public API surface
GET /users/{id}POST /usersGET /orders/{id}POST /ordersGET /api/status(legacy monolith)
Step 2: Decide the host strategy
Start simple with one host:
api.example.comfor all public endpoints
If you need separation later, you can introduce additional hosts (for example, admin.example.com).
Step 3: Create path prefixes per backend
/users/→ user service/orders/→ order service/api/→ monolith
Step 4: Add method constraints where needed
For example, if the user service only supports GET and POST on certain endpoints, constrain the route to those methods to avoid forwarding unsupported methods.
Step 5: Set priority/ordering
Place more specific routes first. For example, if you have a special admin endpoint under users:
/users/admin/(higher priority)/users/(lower priority)
URL Rewriting and Path Parameter Forwarding
Gateways often need to rewrite URLs because the public API path structure may not match the internal service’s routes. Two common needs are:
- Strip a prefix before forwarding.
- Preserve and forward path parameters (like
{id}).
Prefix stripping (rewrite)
Suppose your public API is /users/*, but the user service expects routes without the /users prefix (for example, it expects /v1/{id} or just /{id}).
Example rewrite: strip /users and forward the rest:
Incoming: GET /users/123Forward to: http://users-service:8080/123Or rewrite to a versioned internal path:
Incoming: GET /users/123Forward to: http://users-service:8080/v1/users/123Path parameters forwarding
With a parameterized route like /orders/{orderId}, the gateway matches the pattern and captures orderId. Most gateways forward the full path by default, so the backend receives /orders/987 unchanged. If you rewrite, ensure the parameter is preserved in the rewritten path.
Route match: /orders/{orderId}Incoming: GET /orders/987Rewrite to: /v2/orders/987Forward to: http://orders-service:8080/v2/orders/987Practical check: verify that your backend framework’s route template matches what it will actually receive after gateway rewriting.
Basic Load Balancing Across Multiple Upstream Instances
In production, a “backend” is usually a pool of instances (multiple pods/VMs/containers) rather than a single server. The gateway can load balance across them to improve throughput and availability.
Common load balancing strategies
- Round-robin: send requests to each instance in turn.
- Least connections: prefer the instance with fewer active connections.
- Random: simple distribution, sometimes good enough.
- Hash-based (sticky-ish): choose instance based on a key (client IP, header, cookie) to improve cache locality or session affinity.
Example upstream pool for the user service:
users-service upstream pool: - http://10.0.1.10:8080 - http://10.0.1.11:8080 - http://10.0.1.12:8080Strategy: round-robinWhen the route /users/* matches, the gateway picks one healthy instance from the pool and forwards the request.
Health Checks (Conceptual) and Why They Matter for Routing
Health checks help the gateway avoid sending traffic to broken instances. Conceptually, the gateway maintains a list of upstream targets and marks them healthy/unhealthy based on checks.
Types of health checks
- Active checks: the gateway periodically calls a health endpoint (for example,
GET /health) on each instance. - Passive checks: the gateway infers health from real traffic failures (timeouts, connection errors, repeated 5xx responses).
Typical behavior: if an instance fails health checks, it is temporarily removed from the load balancing rotation. When it recovers, it is added back.
Practical tip: ensure each service exposes a lightweight health endpoint that does not depend on slow downstream calls unless you intentionally want to reflect dependency health.
Putting It Together: A Practical Routing Layout
The following layout demonstrates the requested patterns: a monolith catch-all, two microservices, and serverless routing.
Example route set (conceptual)
Host: api.example.com1) Path: /users/* Methods: GET, POST, PUT, DELETE Upstream: users-service pool Rewrite: strip /users (optional)2) Path: /orders/* Methods: GET, POST, PUT, DELETE Upstream: orders-service pool Rewrite: strip /orders (optional)3) Path: /api/* Methods: ANY Upstream: monolith pool Rewrite: none4) Path: /functions/user-created Methods: POST Upstream: serverless function invoke endpoint Rewrite: map to provider-specific invoke pathPattern: /api/* to a monolith
This is useful when you are migrating gradually. You can keep legacy endpoints under /api/ while moving new domains (users, orders) to dedicated services.
Incoming: GET /api/statusForward to: http://monolith:8080/api/statusPattern: /users/* to a user service
All user-related endpoints route to the user service. If the user service is versioned internally, rewrite accordingly.
Incoming: POST /usersForward to: http://users-service:8080/v1/usersPattern: /orders/* to an order service
All order-related endpoints route to the order service, with path parameters preserved.
Incoming: GET /orders/987Forward to: http://orders-service:8080/v1/orders/987Pattern: routing to serverless functions
Gateways can route certain endpoints to serverless functions, often for event handlers, webhooks, or lightweight glue logic. The gateway still matches host/path/method, then forwards to a function invocation endpoint (which may be HTTP-based or provider-specific).
Incoming: POST /functions/user-createdGateway action: invoke function user-createdForward to: serverless invoke URL (provider-specific)Body/headers: forwarded (possibly with added auth headers)Practical tip: keep serverless routes narrow and explicit (exact paths and strict methods) to avoid accidental invocation from broad wildcard routes.