A spec with a single hard-coded production URL is awkward for everyone. The frontend dev wants to point generated calls at a local mock, CI wants staging, the docs "Try it" panel needs a reachable host, and multi-region customers each hit a different subdomain. The servers array exists precisely so one document describes all of these without forking the spec. Get it slightly wrong, and every path gains a doubled prefix or a missing slash.

The basics: one or more base URLs

servers replaces the OpenAPI 2.0 host, basePath, and schemes fields with a list of base URLs. Every operation path is resolved relative to the chosen server:

openapi: 3.1.0
servers:
  - url: https://api.example.com/v1
    description: Production
  - url: https://api.staging.example.com/v1
    description: Staging
  - url: http://localhost:4010/v1
    description: Local mock
paths:
  /users:
    get: ...

A client or docs renderer resolves GET /users against the selected entry, giving https://api.example.com/v1/users in production and http://localhost:4010/v1/users against the mock. Put production first when the tool defaults to the first entry, but always include the local mock so generated clients and mock servers agree on the shape of the URL.

Server variables for regions and tenants

When the host varies by a bounded set of values, template it with a variable and enumerate the options. Clients and renderers turn this into a dropdown:

servers:
  - url: https://{region}.api.example.com/v1
    description: Regional endpoint (pick the region closest to your data residency).
    variables:
      region:
        enum: [us-east, eu-west, ap-northeast]
        default: us-east
        description: AWS region hosting the endpoint.
  - url: https://{tenant}.example.com/v1
    description: Per-tenant vanity host.
    variables:
      tenant:
        default: acme
        description: Your organization slug.

Use a variable with an enum when the set is known and small (regions, environments). Use a free-text variable (no enum, just a default) only for values only the customer knows, like a tenant slug; the default keeps the document usable without a real tenant.

Variables can appear in the host, the path, or both. Keep them out of query strings; query parameters belong on operations.

Relative servers and same-origin docs

A server URL may be relative, which is the cleanest setup when docs are served from the same origin as the API or when a spec is deployed across environments without changes:

servers:
  - url: /v1
    description: Same origin as the documentation page.

If the docs live at https://app.example.com/docs, calls resolve to https://app.example.com/v1/.... This removes hard-coded hosts from specs promoted through environments and works well behind a reverse proxy or on serverless platforms where the origin is assigned at deploy time. For local file-based docs (file://), a relative server has no origin to resolve against, so pair it with an absolute localhost entry or open the docs over HTTP.

Operation- and path-level servers

servers can also appear on a path item or an individual operation, which is how you model an endpoint served from a different host, such as a file service, a webhook receiver, or a legacy system:

paths:
  /uploads:
    servers:
      - url: https://uploads.example.com
    post: ...
  /legacy/report:
    get:
      servers:
        - url: https://legacy.example.com
      ...

Use this sparingly. Most APIs belong on one base URL; scattering operation-level servers makes generated clients configure multiple hosts and confuses the docs "try it" panel. Reach for it only when the host genuinely differs, and document why.

The prefix and trailing-slash traps

Two mechanical mistakes generate a large share of "works in the mock, 404 in prod" bugs:

  • Doubled or missing prefix. If the server URL ends with /v1 and a path is written as /v1/users, the resolved URL contains /v1/v1/users. Pick one owner of the prefix (almost always the server) and write operation paths without it. Conversely, if the server has no /v1, the paths must include it or the version disappears.
  • Slash joining. RFC 3986 resolution treats the server path as a directory. A server of https://api.example.com/v1 with operation /users yields /v1/users; a server of https://api.example.com/v1/ behaves slightly differently in naive joiners. Standardize on no trailing slash on the server URL and a leading slash on every path.
  • Protocol and port. Include the scheme explicitly; an entry without one is resolved relative to the docs origin. Include the port for local mocks (http://localhost:4010) so it is not assumed to be 443.

Validate resolution with a generated client and a mock rather than eyeballing it; print the final URL for one operation in each environment.

Security and the server

Security schemes are independent of servers, but a token issued for one audience is often invalid on another. When listing staging and production, document that credentials are environment-specific so a production token is not pasted into a local mock. For per-tenant hosts, make sure the authorization scheme (for example a bearer token or a tenant claim) is documented at the operation level and not implied by the subdomain alone.

What codegen, mocks, and docs do

  • Generators expose the server list as configurable base URLs or environments; a well-formed list lets the same SDK point at a mock in tests and production in the deploy with no code change. A missing localhost entry pushes every team to hand-edit the generated base URL.
  • Mock servers read the same servers entries to know which base path to serve, so listing the mock URL keeps the contract and the mock aligned.
  • Documentation renderers show a server picker and expand variables into dropdowns for regions; free-text variables render as an input.
  • When reverse-engineering a spec from code, a sound scanner reads the framework's mount prefix, environment-configured host, and regional routing instead of baking localhost:3000 from the developer's machine into the contract; an unknown deployment host should be a clearly marked default rather than a guessed production domain.

Checklist

  1. List production, staging, and a local mock server, with production first; keep one owner of any /v1 prefix.
  2. Template bounded host differences (regions, environments) as server variables with an enum and default.
  3. Use a free-text variable only for customer-specific values like a tenant slug, always with a usable default.
  4. Use a relative /v1 server for same-origin and environment-portable docs; pair it with an absolute localhost entry for local files.
  5. Apply path- or operation-level servers only when the host genuinely differs, and say why.
  6. Standardize no trailing slash on server URLs and leading slashes on paths; verify resolved URLs with a client and a mock.
  7. Note that credentials are environment-specific and keep security schemes independent of hosts.

Get these right and a single spec drives local mocks, staging tests, regional production traffic, and the docs try-it panel without anyone hand-editing a base URL.

You can define multiple servers and variables, generate an SDK preconfigured for each environment, and run a local mock on the matching base URL in one local-first workspace, right in your browser. For picking a mock server that honors these base URLs, see the best OpenAPI mock servers tested on one real spec.