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-termination plugin 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-termination still needs a service with a url. Kong’s schema requires every route to attach to a service, and a service must declare a url or host. Use an unreachable placeholder like http://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 with route ... has no service.

  • Route specificity, not file order, decides matching. Kong does not evaluate routes top-to-bottom like an Nginx location list. It scores routes by path length, method, and header specificity, so /api/v2/feature-flags reliably beats /api/v2 regardless of ordering in kong.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.


← Back to Local API Gateway Routing