One OpenAPI Spec, Three Environments: Using Server Variables to Switch Base URLs
Use OpenAPI 3.0 server variables to maintain a single spec for dev, staging, and production. Swagger UI and Redoc render a live selector; generated SDKs need regeneration to honor the switch. Includes a CI smoke test and linting guardrails.
02 Oct 2026, 23:32 UTC

The problem: duplicated specs for every environment
Most teams start with a single OpenAPI document. Then staging needs a different base URL than production, so someone copies the file and edits the servers[0].url. Six months later you have three YAML files drifting out of sync, and the CI pipeline that publishes docs has to know which copy to render. The OpenAPI 3.0 servers.variables mechanism solves this by letting you declare one server entry with a placeholder, then swap the host at runtime via a query parameter or the Swagger UI dropdown.
How server variables work
Instead of hard-coding https://api.example.com/v1, you define a variable named env and reference it in the server URL template. The spec declares the allowed values and a default. When a consumer opens the rendered documentation, the UI shows a selector; choosing "Production" rewrites the base path for every Try it out call without changing the spec file.
openapi: 3.0.3
info:
title: Orders API
version: 1.0.0
servers:
- url: 'https://{env}.api.example.com/v1'
description: Environment-specific endpoint
variables:
env:
enum:
- dev
- staging
- prod
default: dev
description: Deployment environment
paths:
/orders:
get:
summary: List orders
responses:
'200':
description: OK
The variables block lives inside the server object. Each key under variables becomes a placeholder in the url template (the curly-brace syntax). The enum restricts the dropdown to known hosts; default picks the one used when no explicit choice is made.
Tooling that respects the selector
Swagger UI, Redoc, and the OpenAPI Generator CLI all read servers.variables natively. In Swagger UI the dropdown appears at the top of the page; appending ?server=prod to the docs URL pre-selects production. Redoc renders a similar control. For generated clients, the story is mixed: the generator emits a Configuration class with a setBasePath method, but many templates still hard-code the first server URL. You must regenerate SDKs after adding variables, or patch the generated code to read the variable at runtime.
Worked example: verifying the switch in CI
Add a step to your docs pipeline that serves the spec with Swagger UI and asserts the selector works:
# Dockerfile snippet for docs preview
FROM swaggerapi/swagger-ui:v5.11.0
COPY openapi.yaml /usr/share/nginx/html/spec.yaml
ENV SWAGGER_UI_CONFIG_URL=/spec.yaml
EXPOSE 8080
Run the container, then curl the health endpoint with the query parameter:
docker run -d -p 8080:8080 my-docs
sleep 3
curl -s 'http://localhost:8080/?server=prod' | grep -c 'https://prod.api.example.com/v1'
# expect 1
If the grep returns 1, the UI has rewritten the base URL. This test catches missing variables blocks or typos in the enum before the spec reaches consumers.
Trade-offs and gotchas
- SDK generation lag: Most generators (Java, Kotlin, TypeScript, Go) still bake the first
server.urlinto the client. After you add variables, regenerate and verify the generatedConfiguration.setBasePathaccepts your enum values. - Query parameters in logs: The
?server=prodswitch appears in browser history, proxy logs, and bookmarks. If your staging and prod hosts differ only by subdomain, that's low risk. If they expose internal network paths, manage the host via environment variables at the gateway layer instead of relying on the spec alone. - Silent fallback: A missing or misspelled variable key causes the renderer to fall back to the first
serverentry without warning. Always lint withspectralorvacuumusing a rule that requires everyserver.variablesreference to have a matching definition.
Closing checklist
- Add a single
serverentry with a templated URL andvariablesblock to your canonical spec. - Run your linter to confirm all placeholders resolve.
- Regenerate every client SDK and verify
setBasePath(or equivalent) exists. - Add the
curlsmoke test to your docs CI job. - Document the
?server=parameter for API consumers who bookmark deep links.
One spec, three environments, zero copy-paste drift. That's the promise server variables deliver—if you close the generation and logging gaps.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.