Mocking APIs with Kong Gateway
You want a local gateway that both forwards versioned API paths to a running mock backend and returns canned responses for endpoints that have no backend yet — and you want the whole thing described in one file you can commit. Kong in DB-less declarative mode does exactly this: a single kong.yml defines every service, route, and plugin, and the built-in request-termination plugin lets you stub an endpoint inline without standing up any upstream at all.
Context: why Kong DB-less fits local mocking
Kong’s production reputation is as a database-backed gateway managing hundreds of services. That model is overkill on a laptop — nobody wants a Postgres container just to route /api/v2/* to a mock. DB-less mode removes the database entirely: you set KONG_DATABASE=off, hand Kong a declarative YAML file at startup, and it loads the full routing table into memory. The configuration is reproducible from source control, boots in a couple of seconds, and behaves identically on every machine.
Two capabilities make Kong specifically useful for routing local traffic through a mock API gateway:
- Service-and-route forwarding sends real HTTP traffic to a mock backend such as a WireMock standalone instance, so you get full request matching, templated responses, and scenario state.
- The
request-terminationplugin answers a route directly from Kong. This is the fastest possible stub — no upstream process, no mapping file — perfect for an endpoint your team has agreed on but nobody has built.
The pattern below combines both: most paths flow through to WireMock, while a handful of not-yet-built endpoints are terminated inline. This mirrors the split-routing idea from the local API gateway routing overview, but expressed in Kong’s declarative schema rather than Nginx or Traefik.
Solution
1. Author the declarative kong.yml
Everything Kong needs lives in one file. The _format_version header is mandatory; services hold upstream targets, each with one or more routes, and plugins can attach at the service, route, or global level.
# kong.yml — declarative config loaded in DB-less mode
_format_version: "3.0"
_transform: true
services:
# Forward versioned API traffic to the WireMock backend
- name: mock-backend
url: http://wiremock:8080
routes:
- name: api-v2
paths:
- /api/v2
strip_path: true # remove /api/v2 before forwarding to WireMock
plugins:
- name: cors
config:
origins:
- "http://localhost:5173"
- "http://localhost:3000"
methods:
- GET
- POST
- PUT
- PATCH
- DELETE
- OPTIONS
headers:
- Content-Type
- Authorization
- X-Request-ID
max_age: 3600
# An endpoint with NO real upstream — stubbed inline by request-termination.
# The url below is a placeholder; the plugin short-circuits before Kong dials it.
- name: feature-flags-stub
url: http://placeholder.invalid
routes:
- name: feature-flags
paths:
- /api/v2/feature-flags
strip_path: false
plugins:
- name: request-termination
config:
status_code: 200
content_type: "application/json; charset=utf-8"
body: |
{"flags":{"new-checkout":true,"beta-search":false},"generatedAt":"2025-02-18T00:00:00Z"}
# A deliberately failing endpoint to exercise client error handling
- name: payments-outage-stub
url: http://placeholder.invalid
routes:
- name: payments-outage
paths:
- /api/v2/payments/charge
methods:
- POST
strip_path: false
plugins:
- name: request-termination
config:
status_code: 503
content_type: "application/json; charset=utf-8"
body: |
{"error":"service_unavailable","message":"Payments provider is down (simulated)"}
Two things make the inline stubs work. First, route matching in Kong prefers the most specific path, so /api/v2/feature-flags is selected before the broader /api/v2 route — the terminated stub wins for that exact path while everything else under /api/v2 still flows to WireMock. Second, request-termination runs in Kong’s access phase and returns immediately, so the http://placeholder.invalid upstream is never dialled.
2. Define the Compose stack
Run Kong and WireMock together. Kong reads kong.yml through the KONG_DECLARATIVE_CONFIG variable; nothing else is persisted.
# docker-compose.yml
services:
kong:
image: kong:3.7
environment:
KONG_DATABASE: "off"
KONG_DECLARATIVE_CONFIG: /kong/kong.yml
KONG_PROXY_LISTEN: "0.0.0.0:8000"
KONG_ADMIN_LISTEN: "0.0.0.0:8001"
KONG_LOG_LEVEL: notice
KONG_PROXY_ACCESS_LOG: /dev/stdout
KONG_ADMIN_ACCESS_LOG: /dev/stdout
KONG_PROXY_ERROR_LOG: /dev/stderr
volumes:
- ./kong.yml:/kong/kong.yml:ro
ports:
- "8000:8000" # proxy — application traffic goes here
- "8001:8001" # Admin API
healthcheck:
test: ["CMD", "kong", "health"]
interval: 5s
timeout: 3s
retries: 6
start_period: 5s
depends_on:
wiremock:
condition: service_healthy
networks:
- mocknet
wiremock:
image: wiremock/wiremock:3.13.2
command:
- "--port=8080"
- "--verbose"
volumes:
- ./wiremock/mappings:/home/wiremock/mappings:ro
- ./wiremock/__files:/home/wiremock/__files:ro
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/__admin/health"]
interval: 5s
timeout: 3s
retries: 6
start_period: 10s
networks:
- mocknet
networks:
mocknet:
driver: bridge
Point your application’s API base URL at http://localhost:8000 — Kong’s proxy port — and every request now flows through the gateway.
3. Provide the WireMock mapping for forwarded paths
Because the api-v2 route sets strip_path: true, Kong removes /api/v2 before forwarding. WireMock therefore sees /users, not /api/v2/users:
{
"mappings": [
{
"request": { "method": "GET", "url": "/users" },
"response": {
"status": 200,
"headers": { "Content-Type": "application/json" },
"jsonBody": {
"users": [
{ "id": "u1", "name": "Priya Raman", "role": "admin" },
{ "id": "u2", "name": "Diego Salas", "role": "editor" }
]
}
}
}
]
}
4. Reload configuration without a restart
When you edit kong.yml, apply it without bouncing the container by posting the file to the Admin API:
curl -sf -X POST http://localhost:8001/config \
-F "[email protected]" > /dev/null && echo "Kong config reloaded"
This swaps the in-memory routing table atomically, so long-running dev sessions never drop connections. Externalising the upstream URLs so the same kong.yml works across machines is covered in environment-variable-driven route switching.
Verification
Bring the stack up and confirm all three routing behaviours — forwarded, inline stub, and inline error — in one pass:
docker compose up -d --wait
# 1. Forwarded to WireMock (strip_path removed /api/v2)
curl -s http://localhost:8000/api/v2/users | jq '.users[0].name'
# Expected: "Priya Raman"
# 2. Inline stub from request-termination (no upstream)
curl -s http://localhost:8000/api/v2/feature-flags | jq '.flags["new-checkout"]'
# Expected: true
# 3. Inline simulated outage
curl -s -o /dev/null -w "%{http_code}\n" -X POST http://localhost:8000/api/v2/payments/charge
# Expected: 503
If all three lines match, Kong is routing forwarded paths to WireMock and terminating stubbed paths itself. Confirm the loaded routing table directly from the Admin API:
curl -s http://localhost:8001/routes | jq '.data[].name'
# Expected: "api-v2", "feature-flags", "payments-outage"
Gotchas and edge cases
-
request-terminationstill needs aservicewith aurl. Kong’s schema requires every route to attach to a service, and a service must declare aurlorhost. Use an unreachable placeholder likehttp://placeholder.invalid— the plugin short-circuits in the access phase before Kong ever resolves or dials that host, so the placeholder is never contacted. Omitting the service entirely fails validation at startup withroute ... has no service. -
Route specificity, not file order, decides matching. Kong does not evaluate routes top-to-bottom like an Nginx
locationlist. It scores routes by path length, method, and header specificity, so/api/v2/feature-flagsreliably beats/api/v2regardless of ordering inkong.yml. If a stub is being swallowed by a broader route, check that its path is genuinely more specific rather than reordering the file. This same longest-match behaviour underpins the split routing described in when to use proxy vs inline mocking. -
Declarative config is all-or-nothing. A single YAML syntax error or an unknown plugin name makes Kong refuse to start in DB-less mode — there is no partial load. Validate before running with
docker run --rm -v "$PWD/kong.yml:/kong.yml:ro" kong:3.7 kong config parse /kong.yml, which reports the exact line and field that failed.
FAQ
Do I need a database to run Kong as a local mock gateway?
No. Set KONG_DATABASE=off and point KONG_DECLARATIVE_CONFIG at a kong.yml file. DB-less mode loads the entire configuration into memory at startup, which is ideal for local development because the gateway is fully reproducible from a single version-controlled file with no Postgres container to manage. This is what keeps the dockerized mock environments stack small — Kong plus WireMock, nothing else.
How do I return a stub response from Kong without any backend service?
Attach the request-termination plugin to a route. It short-circuits the request in Kong’s access phase and returns a fixed status code, content type, and body directly from Kong, so you can stub an endpoint before the real backend or a WireMock mapping exists. The route still needs a parent service with a url, but that upstream is never dialled because the plugin responds first.
Can Kong reload declarative config without a restart?
Yes. POST the updated kong.yml to the Admin API endpoint /config (curl -F "[email protected]" http://localhost:8001/config), or run kong reload inside the container. Both apply the new declarative configuration atomically without dropping in-flight connections, so long dev sessions never need a container bounce.
Related
- Nginx Reverse Proxy for Local Mock APIs — the same split-routing idea expressed with
locationblocks andupstreamdirectives - Environment-Variable-Driven Route Switching — parameterise Kong upstreams so one config works in dev and CI
- Routing Local Traffic Through a Mock API Gateway — the generic Nginx walkthrough this Kong setup parallels
← Back to Local API Gateway Routing