OpenAPI Servers and Server Variables Practical Examples

OpenAPI servers and server variables examples show how to configure dynamic base URLs for multi-tenant, regional and staging APIs.

Jump to section

Jump to section

Jump to section

Most APIs start simple with a single `https://api.example.com` base URL, but production systems quickly outgrow this approach. When you're serving thousands of customers across multiple regions, handling different environments, or managing tenant-specific subdomains, you need OpenAPI server variables to define dynamic, templated URLs directly in your specification.

Server variables aren't just about convenience—they're essential for generating SDKs that work seamlessly with complex infrastructure. This guide shows you how to model multi-tenant SaaS architectures, regional deployments, and microservice patterns using practical OpenAPI examples that translate into clean, configurable client libraries.

Summary

When your API serves thousands of customers across multiple regions, a single static base URL like https://api.example.com/v1 just doesn't cut it. OpenAPI servers and server variables are the standard way to define dynamic, templated URLs that handle multi-tenancy, regional deployments, and different environments directly in your spec. This guide provides practical, production-ready examples that show you how to model these complex scenarios, avoid common pitfalls, and generate clean, configurable SDKs that work seamlessly with your infrastructure.

When basic server URLs break at scale

OpenAPI server variables allow you to define dynamic, placeholder parts within your API's base URL. This is essential for handling scenarios like different deployment environments, regional data centers, or customer-specific subdomains, all within a single, clean OpenAPI specification. Without them, you're stuck managing multiple spec files or hardcoding logic, which quickly becomes unmanageable and makes it nearly impossible to generate MCP servers from OpenAPI specs that handle dynamic URLs properly.
A simple servers block works fine for a basic API, but it has its limits. A hard-coded URL creates friction in CI/CD pipelines, forces you to maintain separate documentation for each environment, and leads to inconsistent baseUrl configurations in client SDKs. By using variables, these dynamic parts of your URL become first-class citizens in your spec, which allows tooling like the Stainless SDK generator to automatically produce more intelligent and configurable SDKs.

Multi-tenant SaaS with customer subdomains

For most SaaS products, giving each customer a unique subdomain is a common pattern for isolating tenants. Server variables make it simple to represent this structure.

# openapi.yml
servers:
  - url: 'https://{customer}.api.example.com/{version}'
    variables:
      customer:
        default: 'demo'
        description: 'The customer-specific subdomain.'
      version:
        default: 'v1'
        description: 'The API version.'

This approach keeps your spec clean while supporting an infinite number of customers. Here are a few things to keep in mind during implementation.

Choose variable defaults

Always provide a default value for each variable, like demo or a sandbox tenant. This ensures that documentation tools, API explorers, and curl examples render a valid, working URL out of the box.

Wire per-tenant auth

A dynamic URL must be paired with dynamic authentication. The {customer} variable in the URL should correspond to a specific tenant's API key or an OAuth token with the correct audience claim.

Generate tenant-aware SDK clients

A well-generated SDK will allow users to configure this dynamic part at initialization. For example, here’s how a Stainless-generated TypeScript client might handle a tenant-specific base URL:

import { MyApi } from 'my-api'; // from your generated TypeScript package

const client = new MyApi({
  baseUrl: 'https://acme-corp.api.example.com/v1',
  authToken: 'acme-corp-api-key',
});

A key pitfall to avoid is defining customer with an enum. This would force you to update your OpenAPI spec every time you onboard a new customer, which completely defeats the purpose of using a variable.

Regional APIs with data residency requirements

When you need to comply with data residency laws like GDPR, your API must live in specific geographic regions. You can model this using a variable with a fixed set of possible values via an enum.

# openapi.yml
servers:
  - url: 'https://{region}.api.example.com'
    variables:
      region:
        default: 'us-east-1'
        enum:
          - 'us-east-1'
          - 'eu-central-1'
          - 'ap-southeast-2'
        description: 'The geographic region for data residency.'

This pattern is crucial for global applications. It makes regional deployment a formal part of your API contract, which is a huge win for both compliance and clarity. When you generate an SDK from this spec, it can even expose a typed region option in the client constructor, preventing users from passing invalid values.

In some cases, a specific endpoint might only exist in one region, for example, a GDPR-related data export tool that is only available in the EU. You can handle this by adding a path-level servers override that hardcodes the eu-central-1 region for just that endpoint.

Environment strategies beyond dev and prod

Nearly every API has at least a development and production environment, but staging, QA, and ephemeral preview environments are also common. Server variables are the ideal way to manage these without duplicating your entire spec.

# openapi.yml
servers:
  - url: 'https://{environment}.api.example.com'
    variables:
      environment:
        default: 'sandbox'
        enum:
          - 'sandbox'
          - 'staging'
          - 'production'

This setup integrates perfectly into a CI/CD workflow. Your deployment pipeline can simply substitute the {environment} variable when publishing documentation or running integration tests, and you can integrate SDK snippets with your API docs to ensure code examples work correctly across all environments. For more advanced setups, you can even support ephemeral preview environments for pull requests by using a placeholder like pr-{id} and overriding the variable at runtime.

Use sensible defaults

Your default environment should always be a non-production one, like sandbox. This prevents users who are copying code examples from your documentation from accidentally making calls to your live production API.

Support ephemeral preview apps

For preview environments tied to pull requests, you don't need to add every possible pr-123 to the enum. Instead, your CI process can dynamically construct the URL and pass it to your test suite, overriding the default value.

A common mistake is to include the API version (e.g., v1, v2) as an environment variable. An API version represents a different contract and should be part of the static URL path, not a dynamic variable.

OAuth and authentication server patterns

Often, the server that issues authentication tokens is different from the server that handles API requests, which is an important architectural pattern to understand when you create OpenAPI specs. The servers object can technically hold multiple URLs, but it's better to keep your primary API server as the main entry.

The token URL for OAuth or other authentication flows belongs in the securitySchemes component of your spec, not in the main servers array. This tells tooling where to direct the user for authentication, while keeping the servers block focused on the resource servers.

# openapi.yml
servers:
  - url: 'https://api.example.com'

components:
  securitySchemes:
    oauth2:
      type: oauth2
      flows:
        authorizationCode:
          authorizationUrl: 'https://auth.example.com/oauth2/authorize'
          tokenUrl: 'https://auth.example.com/oauth2/token'
          scopes:
            'read:data': 'Read data'

When an SDK is generated from a spec like this, its built-in authentication helpers can automatically use the correct tokenUrl without requiring the user to manually configure multiple base URLs.

Path-level overrides for microservices

As a monolith API evolves, it's often broken down into microservices, each with its own deployment. You can represent this architecture by defining a global server and then overriding it for specific paths.

# openapi.yml
servers:
  - url: 'https://api.example.com' # Default server

paths:
  /users:
    get:
      # Uses the default server
      ...
  /payments:
    post:
      # Overrides the default server for just this path
      servers:
        - url: 'https://payments.api.example.com'

This approach offers a few benefits:

  • Isolation: The payments service can be deployed and scaled independently.

  • Clarity: The spec clearly documents which endpoints are served by which service.

  • Developer Experience: SDK generation tools can use these overrides to route requests to the correct host automatically, creating a seamless experience for the end user who just sees a single, unified client.

What not to do with server variables

While powerful, server variables can be misused. Here are a few anti-patterns to avoid to keep your spec maintainable and your users happy.

  • Don't encode API versions. A major version like /v2 is a new API contract and should be in the static part of the URL, not a variable.

  • Don't overload variables for routing. A variable should represent an environment or tenant, not business logic.

  • Don't forget default values. Omitting a default breaks many documentation tools and can confuse users trying to make their first API call.

  • Don't enumerate tenants. Never use an enum for a list that grows, like customers or dynamic regions. This creates unnecessary spec updates.

  • Don't explode combinations. A server URL with four different variables can create dozens of possible combinations, making your API difficult to test and understand.

Each of these missteps complicates the developer experience and can lead to poorly generated, confusing SDKs, which is why your API isn't finished until the SDK ships with proper server variable support.

Frequently asked questions about complex server configurations

Do server variables change how generated SDKs set base URLs?

Yes, SDK generators typically use the first URL in your servers array, with its default variables, to set the default baseUrl for the client. All modern SDKs then allow the user to easily override this baseUrl at initialization.

How do I test every server variation in CI?

You can use a matrix strategy in your CI/CD pipeline. Your test runner can iterate through each enum value for a variable, setting it as an environment variable that your test suite uses to construct the base URL for each run.

Can I combine API gateways with server variables?

Absolutely. Your OpenAPI spec should define the public-facing URL exposed by the API gateway. The gateway is then responsible for routing the request to the correct upstream microservice based on the path or subdomain.

Does using many variables hurt performance?

For the end user, the performance impact is negligible. The variable substitution happens within the client or documentation tool before the request is made, so it doesn't add any network latency.

What’s the safest path from Swagger 2.0 host/basePath to OpenAPI 3 servers?

The safest migration is to combine the host and basePath from your Swagger 2.0 spec into a single url in the OpenAPI 3 servers array, and you should edit configs and OpenAPI specs with branches to test these changes safely before merging. For example, host: api.example.com and basePath: /v1 becomes url: https://api.example.com/v1.

Thoughtful server variable design is a hallmark of a mature, scalable API. When paired with modern tooling, it allows you to ship reliable, multi-environment SDKs without forcing your users to write brittle, hand-rolled client logic.

Ready to generate high-quality SDKs from your OpenAPI spec? Get started for free.