Local API Gateway Routing

This page covers exactly one thing: running a reverse-proxy gateway on your local machine that intercepts outbound API traffic, routes versioned paths to mock backends, and falls back to live or staging services for everything else. It does not cover browser-scoped interceptors or in-process mocking.

Prerequisites

  • Docker Desktop (or Docker Engine + Compose plugin) — v24 or later
  • mkcert installed and mkcert -install already run so your system trusts a local CA
  • A running mock backend on a known local port — for example, a WireMock standalone instance on localhost:8080
  • Basic familiarity with proxy vs inline mocking strategies — the gateway is a proxy-style approach
  • curl available for verification steps

Why a local gateway?

Request interception patterns like MSW work inside the JavaScript runtime: they capture fetch and XMLHttpRequest calls from browsers and Node.js processes. A local gateway sits below the runtime at the TCP layer, making it the right choice when traffic comes from clients that never touch the JS stack — native mobile apps running on a simulator pointed at your dev machine, backend microservices calling each other, CLI tools, or gRPC clients using generated stubs.

The trade-off is setup cost versus coverage breadth. A gateway adds a configuration layer you must maintain, but once it is in place every client on your machine shares the same routing table with no per-client changes.

The diagram below shows the routing topology this page builds:

Local API Gateway Routing Topology Traffic from browser, mobile simulator, CLI, and backend services all flows into a local Traefik gateway. The gateway routes /api/v2/* to a local mock server and /auth/* plus everything else to the live staging API. Clients Browser (fetch / XHR) Mobile simulator CLI / backend service gRPC client Local Gateway (Traefik or Nginx) localhost:443 route table + TLS termination Mock backend WireMock · localhost:8080 /api/v2/* → Live / staging api.staging.internal /auth/* + catch-all → All clients share one routing table — no per-client config changes required

Phase 1 — Core setup: Traefik as a local gateway

Traefik is the simplest starting point because it reads route rules from a YAML file at runtime and reloads without a restart. The Docker Compose definition below runs the gateway alongside WireMock.

# docker-compose.yml
services:
  gateway:
    image: traefik:v3.0
    command:
      - "--api.insecure=true"
      - "--providers.file.directory=/etc/traefik/dynamic"
      - "--providers.file.watch=true"
      - "--entrypoints.websecure.address=:443"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.web.http.redirections.entrypoint.to=websecure"
      - "--log.level=INFO"
    ports:
      - "80:80"
      - "443:443"
      - "8081:8080"   # Traefik dashboard
    volumes:
      - "./traefik/dynamic:/etc/traefik/dynamic:ro"
      - "./certs:/certs:ro"
    networks:
      - mocknet

  wiremock:
    image: wiremock/wiremock:3.5.4
    volumes:
      - "./wiremock/mappings:/home/wiremock/mappings:ro"
      - "./wiremock/__files:/home/wiremock/__files:ro"
    networks:
      - mocknet

networks:
  mocknet:
    driver: bridge

The dynamic configuration file controls where each path goes:

# traefik/dynamic/routes.yaml
http:
  routers:
    # Highest-priority rule: header-flagged mock traffic
    mock-env-header:
      rule: "Header(`X-Mock-Env`, `local`)"
      service: mock-backend
      priority: 200
      tls: {}

    # Route versioned API path to WireMock
    mock-api-v2:
      rule: "PathPrefix(`/api/v2`)"
      service: mock-backend
      priority: 100
      middlewares:
        - strip-api-prefix
      tls: {}

    # Pass auth to live staging — never mock credentials
    live-auth:
      rule: "PathPrefix(`/auth`)"
      service: staging-proxy
      priority: 90
      tls: {}

    # Catch-all: forward anything not matched above
    fallback-staging:
      rule: "PathPrefix(`/`)"
      service: staging-proxy
      priority: 10
      tls: {}

  services:
    mock-backend:
      loadBalancer:
        servers:
          - url: "http://wiremock:8080"

    staging-proxy:
      loadBalancer:
        servers:
          - url: "https://api.staging.internal"
        passHostHeader: false

  middlewares:
    strip-api-prefix:
      stripPrefix:
        prefixes:
          - "/api/v2"

tls:
  certificates:
    - certFile: "/certs/local.dev.crt"
      keyFile: "/certs/local.dev.key"

Generate the certificate with mkcert:

# Run once — mkcert -install must already have been run
mkcert -cert-file certs/local.dev.crt -key-file certs/local.dev.key \
  "local.dev" "*.local.dev" "localhost" "127.0.0.1"

Add local.dev to /etc/hosts:

echo "127.0.0.1  local.dev api.local.dev" | sudo tee -a /etc/hosts

Phase 2 — Configuration and wiring

Environment-variable-driven route switching

Hard-coding upstream URLs in routes.yaml prevents reuse across CI and production-preview environments. Externalise URLs through environment variables and use a template step:

# .env.local
MOCK_BACKEND_URL=http://wiremock:8080
STAGING_URL=https://api.staging.internal
MOCK_ENV_NAME=local
# scripts/render-traefik-config.sh
#!/usr/bin/env bash
set -euo pipefail
source .env.local
envsubst < traefik/dynamic/routes.yaml.tpl > traefik/dynamic/routes.yaml
echo "Route table written."
# traefik/dynamic/routes.yaml.tpl  (template — variables substituted at startup)
http:
  services:
    mock-backend:
      loadBalancer:
        servers:
          - url: "${MOCK_BACKEND_URL}"
    staging-proxy:
      loadBalancer:
        servers:
          - url: "${STAGING_URL}"

Header-based environment switching

The X-Mock-Env: local header lets individual requests opt into mock routing without touching the URL. This is valuable when a single test session needs to mix mocked and live responses:

# Request routed to WireMock
curl -H "X-Mock-Env: local" https://local.dev/api/payments

# Same path, no header — falls through to staging
curl https://local.dev/api/payments

Your WireMock standalone configuration must include mappings for the stripped paths (/payments, not /api/v2/payments) because the strip-api-prefix middleware removes the prefix before forwarding.

gRPC routing with Traefik

Traefik v3 treats gRPC as a first-class protocol. Add an IngressRoute with the h2c scheme to forward protobuf traffic to a mock gRPC server:

# traefik/dynamic/grpc-routes.yaml
http:
  routers:
    grpc-mock:
      rule: "PathPrefix(`/com.example.PaymentService`)"
      service: grpc-mock-backend
      priority: 150
      tls: {}

  services:
    grpc-mock-backend:
      loadBalancer:
        servers:
          - url: "h2c://grpc-mock-server:50051"

Phase 3 — Integration with the mock stack and CI

Connecting to the broader mock lifecycle

The gateway is a long-running process that must start before any test suite and shut down cleanly afterwards. Add lifecycle scripts to package.json so the mock stack spins up and tears down with your dev and CI commands:

{
  "scripts": {
    "mock:up": "docker compose up -d gateway wiremock && scripts/wait-for-gateway.sh",
    "mock:down": "docker compose down --remove-orphans",
    "dev": "npm run mock:up && vite",
    "test:integration": "npm run mock:up && vitest run --reporter=verbose; npm run mock:down"
  }
}
#!/usr/bin/env bash
# scripts/wait-for-gateway.sh
set -euo pipefail
echo "Waiting for gateway..."
until curl -sk https://local.dev/health > /dev/null 2>&1; do
  sleep 1
done
echo "Gateway ready."

CI pipeline (GitHub Actions)

# .github/workflows/integration.yml
name: Integration tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install mkcert
        run: |
          sudo apt-get install -y libnss3-tools
          wget -q https://dl.filippo.io/mkcert/latest?for=linux/amd64 -O mkcert
          chmod +x mkcert && sudo mv mkcert /usr/local/bin/
          mkcert -install

      - name: Generate local certs
        run: |
          mkdir -p certs
          mkcert -cert-file certs/local.dev.crt -key-file certs/local.dev.key \
            local.dev "*.local.dev" localhost 127.0.0.1

      - name: Add hosts entry
        run: echo "127.0.0.1  local.dev api.local.dev" | sudo tee -a /etc/hosts

      - name: Start mock stack
        run: docker compose up -d gateway wiremock

      - name: Wait for gateway
        run: bash scripts/wait-for-gateway.sh

      - uses: actions/setup-node@v4
        with:
          node-version: "20"
          cache: "npm"

      - run: npm ci

      - name: Run integration tests
        run: npm run test:integration

      - name: Tear down
        if: always()
        run: docker compose down --remove-orphans

For dockerized mock environments that need the gateway to persist across multiple test suites in one CI job, use a service container rather than a step-level compose invocation — this avoids repeated startup overhead.


Verification steps

Run these commands after npm run mock:up to confirm the gateway is routing correctly.

  • HTTPS reachability:
    curl -sv https://local.dev/health 2>&1 | grep "< HTTP"
    # Expected: < HTTP/2 200
  • Mock path routing (/api/v2/* → WireMock):
    curl -s https://local.dev/api/v2/users | jq .
    # Expected: WireMock response body per your mapping
  • Header-based routing to mock:
    curl -s -H "X-Mock-Env: local" https://local.dev/api/payments | jq .
    # Expected: WireMock response
  • Live passthrough (/auth/* → staging):
    curl -sv https://local.dev/auth/token 2>&1 | grep "x-served-by"
    # Expected: staging upstream header
  • Traefik dashboard accessible:
    curl -s http://localhost:8081/api/overview | jq .http.routers.total
    # Expected: integer matching your router count
  • No routing loops (check logs):
    docker compose logs gateway | grep -i "loop\|cycle"
    # Expected: no output

Troubleshooting

curl: (60) SSL certificate problem: unable to get local issuer certificate

The system trust store does not include the mkcert CA. Run mkcert -install again after installing mkcert (it must be run as the same user that runs curl). On Linux, also run update-ca-certificates. Inside a Docker container, copy the CA root with:

docker exec gateway sh -c \
  "cp /certs/rootCA.pem /usr/local/share/ca-certificates/mkcert-root.crt && update-ca-certificates"

404 page not found from WireMock when the path looks correct

The strip-api-prefix middleware removes /api/v2 before forwarding. Your WireMock mapping must match the stripped path. If WireMock receives GET /users but your mapping registers GET /api/v2/users, it will 404. Confirm the forwarded path with:

docker compose logs wiremock | grep "Request received"

Traefik router shows status: disabled in dashboard

Traefik disables routers that reference an undefined service. Check that every service: name in routes.yaml has a matching entry under services:. Names are case-sensitive.

ERR_TOO_MANY_REDIRECTS in browser

The HTTP-to-HTTPS redirect entrypoint is forwarding to itself. Ensure that entrypoints.web.http.redirections.entrypoint.to is set to websecure (port 443), not web (port 80), and that your application is requesting https:// URLs when talking to the gateway.

Mock response arrives but without CORS headers

Traefik does not automatically inject CORS headers. Add a middleware:

# traefik/dynamic/routes.yaml — add to middlewares section
cors-headers:
  headers:
    accessControlAllowOriginList:
      - "http://localhost:5173"
      - "https://local.dev"
    accessControlAllowHeaders:
      - "Content-Type"
      - "Authorization"
      - "X-Mock-Env"
    accessControlAllowMethods:
      - "GET"
      - "POST"
      - "PUT"
      - "DELETE"
      - "OPTIONS"
    accessControlMaxAge: 300

Then add cors-headers to the middlewares list on each affected router.


When to advance

The gateway is correctly implemented when:

  • Every path in your OpenAPI spec that maps to a mock returns the expected WireMock response via https://local.dev
  • Auth endpoints reliably reach the staging upstream (confirm via x-served-by headers or staging-side logs)
  • npm run test:integration runs green in both local and CI environments without mock:up being called manually
  • The Traefik dashboard shows all routers in enabled state with correct upstream resolution
  • Structured gateway logs capture method, path, and upstream_response_time for every request, giving you an audit trail for debugging contract drift

Once these signals are met, you are ready to add concrete how-to pages for individual gateway tools — for example, Nginx reverse proxy configuration or Kong plugin-based routing — and to connect the gateway to dynamic response shaping so WireMock returns scenario-specific payloads based on request context injected at the gateway layer.


FAQ

When should I use a local gateway instead of MSW or a browser interceptor?

Use a local gateway when you need to intercept traffic from clients that bypass the JavaScript network stack: native mobile apps, CLI tools, backend services, or gRPC clients. MSW handler configuration excels at browser and Node.js fetch/XHR interception but cannot see raw TCP connections from other runtimes. The two approaches are complementary — a team with a React frontend and a mobile app often runs MSW for browser tests and a local gateway for mobile simulator tests against the same WireMock mapping directory.

Can the gateway route some paths to real staging endpoints while mocking others?

Yes. Split-routing is one of the primary use cases. Define higher-priority rules for paths you want to mock (e.g. /api/v2/payments) and a lower-priority catch-all that proxies everything else to your staging URL. Traefik applies longest-prefix matching so rules never overlap ambiguously. This is the same selective-mock pattern described in when to use proxy vs inline mocking.

How do I make the gateway trust my local TLS certificate?

Generate a certificate with mkcert and run mkcert -install to add its CA to your system and browser trust stores. Reference the generated .crt and .key files in your proxy TLS configuration. For Docker-based gateways, mount the certificate files into the container and set the CA bundle path via environment variables. The certs/ directory created in Phase 1 handles this for both Traefik and browser clients that inherit the system trust store.

Does adding a gateway slow down local development iteration?

Traefik supports live configuration reloading — updating routes.yaml takes effect in under a second with no container restart (the --providers.file.watch=true flag enables this). Nginx requires nginx -s reload, which is near-instant. Neither tool adds measurable latency to proxied requests on localhost; the TCP round-trip within the loopback interface is sub-millisecond. The startup time of the Docker containers (typically 2–4 seconds for Traefik + WireMock together) is the only meaningful overhead, and the wait-for-gateway.sh script absorbs it automatically.


← Back to Tool-Specific Implementation & Setup