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 && echoVerify 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}"
doneExpected: 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 .urlExpected: 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_KEYExpected: each deletion succeeds, and all three /version/ paths return 404 afterward.
Troubleshoot
| Symptom | Cause | Fix |
|---|---|---|
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. |
Related
- APISIX Expressions — expression operators and request variables used by query- and header-based routes.
proxy-rewriteplugin reference (opens in Plugin Hub docs) — map a public version to a different upstream path or host.- Conditional Traffic Management — combine request conditions with weighted routing or actions.