a6-recipe-api-versioning
a6-recipe-api-versioning
Overview
API versioning allows you to evolve your API without breaking existing clients. APISIX supports multiple versioning strategies through routing rules, header matching, and traffic splitting — all configurable via the a6 CLI.
Strategies covered:
- URI path versioning —
/v1/users,/v2/users - Header-based versioning —
Accept: application/vnd.api.v2+json - Query parameter versioning —
?version=2 - Gradual migration — weighted traffic split between versions
- Version deprecation — redirect old versions to new
When to Use
- Introducing breaking changes to an existing API
- Running multiple API versions simultaneously
- Gradually migrating clients from v1 to v2
- Deprecating old API versions with user-friendly redirects
Approach A: URI Path Versioning
The most common pattern. Each version has its own URI prefix, and
proxy-rewrite strips the version prefix before forwarding to the backend.
1. Create versioned upstreams
a6 upstream create -f - <<'EOF'
{
"id": "api-v1",
"type": "roundrobin",
"nodes": { "api-v1-backend:8080": 1 }
}
EOF
a6 upstream create -f - <<'EOF'
{
"id": "api-v2",
"type": "roundrobin",
"nodes": { "api-v2-backend:8080": 1 }
}
EOF
2. Create versioned routes with URI rewriting
# v1: /v1/users/123 → /users/123 on api-v1 backend
a6 route create -f - <<'EOF'
{
"id": "route-v1",
"uri": "/v1/*",
"upstream_id": "api-v1",
"plugins": {
"proxy-rewrite": {
"regex_uri": ["^/v1/(.*)", "/$1"]
}
}
}
EOF
# v2: /v2/users/123 → /users/123 on api-v2 backend
a6 route create -f - <<'EOF'
{
"id": "route-v2",
"uri": "/v2/*",
"upstream_id": "api-v2",
"plugins": {
"proxy-rewrite": {
"regex_uri": ["^/v2/(.*)", "/$1"]
}
}
}
EOF
Clients call /v1/users or /v2/users, and the backend always sees /users.
Approach B: Header-Based Versioning
Route based on the Accept header using traffic-split with vars matching.
A single URI serves multiple versions.
a6 route create -f - <<'EOF'
{
"uri": "/api/*",
"plugins": {
"traffic-split": {
"rules": [
{
"match": [
{ "vars": [["http_accept", "~~", "application/vnd\\.api\\.v2\\+json"]] }
],
"weighted_upstreams": [
{
"upstream": {
"type": "roundrobin",
"nodes": { "api-v2-backend:8080": 1 }
},
"weight": 1
}
]
}
]
}
},
"upstream": {
"type": "roundrobin",
"nodes": { "api-v1-backend:8080": 1 }
}
}
EOF
Accept: application/vnd.api.v2+json→ v2 backend- Any other
Acceptvalue → v1 backend (default upstream) ~~is the regex match operator in APISIX vars expressions
Approach C: Query Parameter Versioning
Route based on ?version=2 query parameter.
a6 route create -f - <<'EOF'
{
"uri": "/api/*",
"plugins": {
"traffic-split": {
"rules": [
{
"match": [
{ "vars": [["arg_version", "==", "2"]] }
],
"weighted_upstreams": [
{
"upstream": {
"type": "roundrobin",
"nodes": { "api-v2-backend:8080": 1 }
},
"weight": 1
}
]
}
]
}
},
"upstream": {
"type": "roundrobin",
"nodes": { "api-v1-backend:8080": 1 }
}
}
EOF
/api/users?version=2→ v2 backend/api/usersor/api/users?version=1→ v1 backend
Gradual Version Migration
Use weighted traffic splitting to gradually shift traffic from v1 to v2.
Start: 90% v1, 10% v2
a6 route create -f - <<'EOF'
{
"id": "api-migration",
"uri": "/api/*",
"plugins": {
"traffic-split": {
"rules": [
{
"weighted_upstreams": [
{
"upstream": {
"type": "roundrobin",
"nodes": { "api-v2-backend:8080": 1 }
},
"weight": 1
},
{ "weight": 9 }
]
}
]
}
},
"upstream": {
"type": "roundrobin",
"nodes": { "api-v1-backend:8080": 1 }
}
}
EOF
Shift to 50/50
a6 route update api-migration -f - <<'EOF'
{
"plugins": {
"traffic-split": {
"rules": [
{
"weighted_upstreams": [
{
"upstream": {
"type": "roundrobin",
"nodes": { "api-v2-backend:8080": 1 }
},
"weight": 1
},
{ "weight": 1 }
]
}
]
}
}
}
EOF
Complete: 100% v2
a6 route update api-migration -f - <<'EOF'
{
"upstream": {
"type": "roundrobin",
"nodes": { "api-v2-backend:8080": 1 }
},
"plugins": {}
}
EOF
Version Deprecation with Redirect
When sunsetting v1, redirect clients to v2 with a 301 Moved Permanently.
a6 route update route-v1 -f - <<'EOF'
{
"uri": "/v1/*",
"plugins": {
"redirect": {
"regex_uri": ["^/v1/(.*)", "/v2/$1"],
"ret_code": 301
}
}
}
EOF
Clients calling /v1/users receive:
HTTP/1.1 301 Moved Permanently
Location: /v2/users
Declarative Versioning Config
# apisix-versioning.yaml
upstreams:
- id: api-v1
type: roundrobin
nodes:
"api-v1-backend:8080": 1
- id: api-v2
type: roundrobin
nodes:
"api-v2-backend:8080": 1
routes:
- id: route-v1
uri: "/v1/*"
upstream_id: api-v1
plugins:
proxy-rewrite:
regex_uri: ["^/v1/(.*)", "/$1"]
- id: route-v2
uri: "/v2/*"
upstream_id: api-v2
plugins:
proxy-rewrite:
regex_uri: ["^/v2/(.*)", "/$1"]
a6 config diff -f apisix-versioning.yaml
a6 config sync -f apisix-versioning.yaml
Gotchas
regex_uriis an array of two strings —["pattern", "replacement"], not an object. The pattern is a Lua regex (PCRE-compatible).- traffic-split weight semantics — a
weighted_upstreamsentry without anupstreamfield means "use the route's default upstream". Weight9+ weight1= 90%/10%. ~~operator — regex match in vars expressions. Must double-escape backslashes in JSON:"application/vnd\\\\.api\\\\.v2\\\\+json".- Order matters — traffic-split rules are evaluated top-down. First matching rule wins.
- URI rewrite happens before upstream —
proxy-rewritechanges the URI that the backend sees, not the URI used for route matching. - redirect plugin is terminal — when redirect is active, the request never reaches an upstream. Remove the upstream_id to avoid confusion.
Verification
# Test URI path versioning
curl http://localhost:9080/v1/users # → v1 backend
curl http://localhost:9080/v2/users # → v2 backend
# Test header-based versioning
curl -H "Accept: application/vnd.api.v2+json" http://localhost:9080/api/users # → v2
curl http://localhost:9080/api/users # → v1 (default)
# Test query parameter versioning
curl "http://localhost:9080/api/users?version=2" # → v2
curl http://localhost:9080/api/users # → v1
# Test redirect (deprecation)
curl -v http://localhost:9080/v1/users # → 301 to /v2/users