Routing Local Traffic Through a Mock API Gateway
Your application sends requests to https://api.example.com/v1/users and you want every one of those calls to hit a local mock instead — without changing a single line of application source code. Without a local gateway layer in place, developers either hardcode localhost URLs into the app (breaking CI) or run against real APIs (causing flaky, rate-limited tests). This page shows exactly how to drop Nginx in as a transparent local gateway that intercepts traffic and forwards it to any mock server running on your machine.
Why this scenario arises
The problem appears most often in three situations:
- A frontend app reads its API base URL from
REACT_APP_API_URLorVITE_API_URL, and that variable is already set to a production or staging hostname in a shared.envfile. - A backend microservice calls downstream services via hardcoded
http://service-name:8080DNS names that only resolve inside a production Kubernetes cluster. - A mobile or desktop client uses a certificate-pinned HTTPS endpoint that cannot be trivially overridden at the call-site level.
In all three cases, inserting a local reverse proxy between the application and the network resolves the problem at the infrastructure level. The proxy vs inline mocking strategies comparison covers when this approach is preferable to in-process interception; if you need to intercept fetch calls without a separate process, see request interception patterns instead.
Architecture: how traffic flows through the local gateway
The diagram below shows the routing path from your running application through Nginx to either the mock server or (for non-mocked paths) through to a real upstream, along with the DNS and port mapping that makes it work.
The key insight is that Nginx never touches the application’s process — the app continues calling http://localhost:8080/api/v1/users exactly as configured. Nginx handles path inspection, header injection, and upstream selection entirely at the network layer.
Solution
1. Install Nginx and verify ports
# macOS
brew install nginx
# Debian/Ubuntu
sudo apt-get install -y nginx
# Confirm ports 8080 and 3000 are free
lsof -i :8080 -i :3000
If either port is occupied, terminate the conflicting process with kill -9 <PID> or change the ports in the configuration below.
2. Write the gateway configuration
Save the following as nginx-mock-gateway.conf in your project root. This is a complete, drop-in configuration — no ellipsis placeholders, no manual edits required beyond the port numbers.
# nginx-mock-gateway.conf
# Run with: nginx -c $(pwd)/nginx-mock-gateway.conf
worker_processes 1;
error_log /tmp/nginx-mock-error.log warn;
pid /tmp/nginx-mock.pid;
events {
worker_connections 64;
}
http {
access_log /tmp/nginx-mock-access.log;
# Mock server upstream
upstream mock_server {
server 127.0.0.1:3000;
keepalive 16;
}
server {
listen 8080;
server_name localhost;
# ── Mocked API paths ─────────────────────────────────────────
location /api/v1/ {
proxy_pass http://mock_server/api/v1/;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# CORS — allow all origins in local dev
add_header Access-Control-Allow-Origin * always;
add_header Access-Control-Allow-Methods 'GET, POST, PUT, PATCH, DELETE, OPTIONS' always;
add_header Access-Control-Allow-Headers 'Content-Type, Authorization, X-Request-ID' always;
# Handle OPTIONS preflight without hitting the mock server
if ($request_method = OPTIONS) {
return 204;
}
# Strip HSTS so the browser doesn't enforce HTTPS for localhost
proxy_hide_header Strict-Transport-Security;
}
# ── Second API version routed to a separate mock instance ────
location /api/v2/ {
proxy_pass http://127.0.0.1:3001/api/v2/;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
add_header Access-Control-Allow-Origin * always;
proxy_hide_header Strict-Transport-Security;
}
# ── Health check endpoint (no upstream) ──────────────────────
location /healthz {
return 200 'gateway-ok\n';
add_header Content-Type text/plain;
}
}
}
3. Start the gateway
# Validate config syntax first
nginx -t -c $(pwd)/nginx-mock-gateway.conf
# Start
nginx -c $(pwd)/nginx-mock-gateway.conf
# Reload config without dropping connections (after edits)
nginx -s reload -c $(pwd)/nginx-mock-gateway.conf
# Stop cleanly
nginx -s quit -c $(pwd)/nginx-mock-gateway.conf
Using -c $(pwd)/nginx-mock-gateway.conf keeps the gateway config isolated from any system-wide Nginx installation. The pid and log paths in /tmp/ ensure no root permissions are needed.
4. Point your application at the gateway
Set the API base URL environment variable before starting your dev server:
# .env.local (Vite / Next.js / Create React App)
VITE_API_BASE_URL=http://localhost:8080
NEXT_PUBLIC_API_BASE_URL=http://localhost:8080
REACT_APP_API_BASE_URL=http://localhost:8080
If your application uses a centralised HTTP client (Axios instance, fetch wrapper, or an OpenAPI-generated client), confirm the base URL is read from the environment rather than hardcoded:
// src/lib/apiClient.ts
import axios from "axios";
export const apiClient = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL ?? "http://localhost:8080",
headers: { "Content-Type": "application/json" },
});
This is the only application-level change required. The network layer abstraction pattern explains how centralising the base URL in one place prevents environment leakage across the codebase.
5. Enable TLS for localhost (optional)
Some OAuth 2.0 libraries, Service Workers, and payment SDKs refuse to operate over plain HTTP even on localhost. Use mkcert to generate a locally-trusted certificate:
# Install mkcert and inject its CA root
brew install mkcert # macOS; see mkcert docs for Linux
mkcert -install
# Generate a cert for localhost
mkcert -key-file /tmp/localhost-key.pem -cert-file /tmp/localhost.pem localhost 127.0.0.1
# Verify the files exist
ls -lh /tmp/localhost*.pem
Add an HTTPS server block to nginx-mock-gateway.conf inside the http {} section:
server {
listen 8443 ssl;
server_name localhost;
ssl_certificate /tmp/localhost.pem;
ssl_certificate_key /tmp/localhost-key.pem;
ssl_protocols TLSv1.2 TLSv1.3;
location /api/v1/ {
proxy_pass http://mock_server/api/v1/;
proxy_set_header Host $host;
add_header Access-Control-Allow-Origin * always;
proxy_hide_header Strict-Transport-Security;
}
}
Update your environment variable to use https://localhost:8443 and restart Nginx.
Verification
Run this single command — it exercises the path-matching, header injection, and mock server response in one shot:
curl -s -o /dev/null -w "%{http_code} | X-Powered-By: %header{x-powered-by}\n" \
http://localhost:8080/api/v1/users
Expected output (WireMock as the mock server):
200 | X-Powered-By: WireMock
Also verify the health check endpoint to confirm Nginx itself is healthy independently of the mock server:
curl http://localhost:8080/healthz
# → gateway-ok
If curl returns 000 (connection refused), Nginx did not start — check /tmp/nginx-mock-error.log. If it returns 502 Bad Gateway, Nginx is running but cannot reach the mock server on port 3000.
Gotchas and edge cases
-
proxy_passtrailing slash matters.proxy_pass http://mock_server/api/v1/rewrites/api/v1/usersto/api/v1/userson the upstream. Omitting the trailing slash (proxy_pass http://mock_server) passes the full original URI, including/api/v1/, which doubles the path prefix if your mock server also has a/api/v1/prefix in its own routing. Match the trailing slash to your mock server’s URL scheme. -
Environment variable not picked up at runtime. Node-based dev servers (Vite, Next.js) read
.env.localonly at startup. If you add or changeVITE_API_BASE_URLafter the dev server is already running, you must restart it. Changes to.env(without the.localsuffix) may also require a fullnode_modules/.cachepurge to take effect. -
Mock server restart drops keepalive connections. The
keepalive 16directive in the upstream block makes Nginx cache connections to the mock server. If the mock server restarts (e.g. WireMock hot-reload,nodemonrestart), Nginx will receive502errors on the cached sockets until they time out. Addproxy_next_upstream error timeoutto yourlocationblock to make Nginx reconnect immediately rather than serving errors. For WireMock-specific lifecycle management, see managing mock server lifecycles in Docker.
FAQ
Why does the browser still call the production API even though Nginx is running?
The browser makes requests to whatever URL is embedded in the JavaScript bundle. If your build tool substituted https://api.example.com into the bundle before you set the environment variable, a full rebuild is needed — not just a Nginx restart. Run grep -r "api.example.com" dist/ to confirm whether the production URL was baked in.
How do I route different API versions to different mock servers?
Add separate location blocks inside the same server {} directive — /api/v1/ forwarding to 127.0.0.1:3000 and /api/v2/ forwarding to 127.0.0.1:3001. The configuration in Step 2 above already includes this pattern. Start two independent mock server processes on those ports and Nginx handles the version split transparently.
Does this approach work with WebSocket connections?
Yes, with one addition. WebSocket upgrades require Upgrade and Connection headers to be passed through:
location /ws/ {
proxy_pass http://mock_server/ws/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
Without these headers, Nginx closes the connection after the initial HTTP handshake and the WebSocket client receives a 101 Switching Protocols failure.
Related
- Local API Gateway Routing — parent overview covering gateway tool selection and environment-variable-driven route switching
- Running WireMock in Docker Compose — containerise the mock server this gateway proxies to
- When to Use Proxy vs Inline Mocking — decision guide for choosing between a gateway proxy and in-process interception
- Managing Mock Server Lifecycles in Docker — coordinate mock server startup/shutdown with the gateway
← Back to Local API Gateway Routing