Docs
Apache APISIXManage traffic and APIsTraffic ManagementImplement API Versioning

Implement API Versioning

Route versioned requests in Apache APISIX by path, query parameter, or request header.

Applies to:Apache APISIX 3.18.0

Roles:Platform engineerAPI developerTime:20 minutesImpact:No restart, no traffic change

Goal

Six isolated routes that demonstrate three API-version selection strategies: version in the path, the version query parameter, or the Api-Version request header. Each strategy sends v1 and v2 requests to a visibly different upstream path so you can prove which route matched.

This guide uses exact paths to keep the examples isolated. In production, choose one public versioning strategy and map each version to its real upstream service or path.

Before you begin

  • You have an Apache APISIX instance and cURL. Get APISIX starts one with Docker.
  • You have jq to inspect Admin API and upstream responses.
  • Route IDs beginning with version- and paths beginning with /version/ are unused.

Set the URLs and read the Admin API key without storing it in shell history:

export APISIX_ADMIN_URL="http://127.0.0.1:9180"
export GATEWAY_URL="http://127.0.0.1:9080"
read -rsp "Admin API key: " APISIX_ADMIN_KEY && export APISIX_ADMIN_KEY && echo

Verify that the six route IDs do not exist:

for route_id in version-path-v1 version-path-v2 \
  version-query-v1 version-query-v2 \
  version-header-v1 version-header-v2; do
  curl -s -o /dev/null -w "${route_id}: %{http_code}\n" \
    "${APISIX_ADMIN_URL}/apisix/admin/routes/${route_id}" \
    -H "X-API-KEY: ${APISIX_ADMIN_KEY}"
done

Expected: every lookup returns 404. Also check for another route already using an example path:

curl -s "${APISIX_ADMIN_URL}/apisix/admin/routes" \
  -H "X-API-KEY: ${APISIX_ADMIN_KEY}" | \
  jq -r '.list[] | select(
    .value.uri == "/version/path/v1" or
    .value.uri == "/version/path/v2" or
    .value.uri == "/version/query" or
    .value.uri == "/version/header"
  ) | .key'

Expected: no output. Stop and choose isolated IDs and paths if either check finds an existing route.

Steps

Create two routes for each versioning strategy. The proxy-rewrite markers make the selected version visible in httpbin's returned url value.

Step 1: Route by path

Put the version directly in the URI:

curl -s "${APISIX_ADMIN_URL}/apisix/admin/routes/version-path-v1" -X PUT \
  -H "X-API-KEY: ${APISIX_ADMIN_KEY}" \
  -H "Content-Type: application/json" \
  -d '{
    "uri": "/version/path/v1",
    "plugins": {"proxy-rewrite": {"uri": "/anything/path-v1"}},
    "upstream": {"nodes": {"httpbin.org:80": 1}, "type": "roundrobin"}
  }'

curl -s "${APISIX_ADMIN_URL}/apisix/admin/routes/version-path-v2" -X PUT \
  -H "X-API-KEY: ${APISIX_ADMIN_KEY}" \
  -H "Content-Type: application/json" \
  -d '{
    "uri": "/version/path/v2",
    "plugins": {"proxy-rewrite": {"uri": "/anything/path-v2"}},
    "upstream": {"nodes": {"httpbin.org:80": 1}, "type": "roundrobin"}
  }'

Expected: both writes return route objects whose IDs end in version-path-v1 and version-path-v2.

What changed: the client selects a version through the path itself.

Step 2: Route by query parameter

Use the same public path for both versions and distinguish the routes with arg_version. APISIX exposes a query parameter named version as that expression variable.

curl -s "${APISIX_ADMIN_URL}/apisix/admin/routes/version-query-v1" -X PUT \
  -H "X-API-KEY: ${APISIX_ADMIN_KEY}" \
  -H "Content-Type: application/json" \
  -d '{
    "uri": "/version/query",
    "vars": [["arg_version", "==", "v1"]],
    "plugins": {"proxy-rewrite": {"uri": "/anything/query-v1"}},
    "upstream": {"nodes": {"httpbin.org:80": 1}, "type": "roundrobin"}
  }'

curl -s "${APISIX_ADMIN_URL}/apisix/admin/routes/version-query-v2" -X PUT \
  -H "X-API-KEY: ${APISIX_ADMIN_KEY}" \
  -H "Content-Type: application/json" \
  -d '{
    "uri": "/version/query",
    "vars": [["arg_version", "==", "v2"]],
    "plugins": {"proxy-rewrite": {"uri": "/anything/query-v2"}},
    "upstream": {"nodes": {"httpbin.org:80": 1}, "type": "roundrobin"}
  }'

Expected: both writes succeed even though the URI is the same, because the route expressions are mutually exclusive.

What changed: /version/query?version=v1 and /version/query?version=v2 now resolve to different routes.

Step 3: Route by request header

APISIX exposes the Api-Version header as http_api_version; HTTP header names are case-insensitive.

curl -s "${APISIX_ADMIN_URL}/apisix/admin/routes/version-header-v1" -X PUT \
  -H "X-API-KEY: ${APISIX_ADMIN_KEY}" \
  -H "Content-Type: application/json" \
  -d '{
    "uri": "/version/header",
    "vars": [["http_api_version", "==", "v1"]],
    "plugins": {"proxy-rewrite": {"uri": "/anything/header-v1"}},
    "upstream": {"nodes": {"httpbin.org:80": 1}, "type": "roundrobin"}
  }'

curl -s "${APISIX_ADMIN_URL}/apisix/admin/routes/version-header-v2" -X PUT \
  -H "X-API-KEY: ${APISIX_ADMIN_KEY}" \
  -H "Content-Type: application/json" \
  -d '{
    "uri": "/version/header",
    "vars": [["http_api_version", "==", "v2"]],
    "plugins": {"proxy-rewrite": {"uri": "/anything/header-v2"}},
    "upstream": {"nodes": {"httpbin.org:80": 1}, "type": "roundrobin"}
  }'

Expected: both writes succeed and the header value decides which route matches.

What changed: clients can keep a stable URL and select the API representation with a header.

Validate

Request each version and print the upstream URL returned by httpbin:

curl -s "${GATEWAY_URL}/version/path/v1" | jq -r .url
curl -s "${GATEWAY_URL}/version/path/v2" | jq -r .url
curl -s "${GATEWAY_URL}/version/query?version=v1" | jq -r .url
curl -s "${GATEWAY_URL}/version/query?version=v2" | jq -r .url
curl -s "${GATEWAY_URL}/version/header" -H "Api-Version: v1" | jq -r .url
curl -s "${GATEWAY_URL}/version/header" -H "Api-Version: v2" | jq -r .url

Expected: the six values end in /anything/path-v1, /anything/path-v2, /anything/query-v1, /anything/query-v2, /anything/header-v1, and /anything/header-v2, respectively.

Verify that a missing or unknown selector does not silently fall through to another version:

curl -s -o /dev/null -w '%{http_code}\n' "${GATEWAY_URL}/version/query"
curl -s -o /dev/null -w '%{http_code}\n' \
  "${GATEWAY_URL}/version/header" -H "Api-Version: v3"

Expected: both requests return 404. Add an explicit default route only if your API's versioning policy defines one.

Roll back / Clean up

Delete only the six routes created by this guide:

for route_id in version-path-v1 version-path-v2 \
  version-query-v1 version-query-v2 \
  version-header-v1 version-header-v2; do
  curl -s -X DELETE \
    "${APISIX_ADMIN_URL}/apisix/admin/routes/${route_id}" \
    -H "X-API-KEY: ${APISIX_ADMIN_KEY}"
done

unset APISIX_ADMIN_KEY

Expected: each deletion succeeds, and all three /version/ paths return 404 afterward.

Troubleshoot

SymptomCauseFix
A route write returns 401.The Admin API key is missing or incorrect.Read the configured key again and send it in X-API-KEY; do not add it to the route itself.
Query or header requests return 404.The selector is absent, misspelled, or not v1/v2.Compare the request with version=v1 or Api-Version: v1 and inspect the route's vars.
Both selector routes appear eligible after you adapt the example.Their expressions overlap. Equal-priority overlapping routes have no useful version precedence contract.Make the expressions mutually exclusive, or set explicit priorities and document the fallback policy.
The response is not JSON or jq prints null.The public sample upstream is unavailable or intercepted by a proxy.Inspect the full response, then substitute a controlled upstream and keep the distinct rewrite markers.