How should I version my API?
Elimination matrix
engineeringapiversioningarchitecturebackend
Choosing the wrong API versioning strategy creates long-term maintenance pain, breaks client integrations, and can stall product evolution. This elimination tree narrows down the right approach—URI path versioning, header versioning, query parameter versioning, or a no-versioning backwards-compatibility contract—by working through the characteristics of your API, its consumers, and your team's operational constraints.
Overview
Full decision path
Start: Q1
Q1 — Do you anticipate introducing breaking changes to this API within the next 12 months?
- A: Yes, breaking changes are likely or certain → outcome URI Path Versioning (/v1/, /v2/) → outcome Header Versioning (Accept / Custom Header) → outcome Query Parameter Versioning (?version=2)
- B: No, we will maintain strict backwards compatibility → outcome No Versioning / Backwards Compatibility Only → outcome URI Path Versioning (/v1/, /v2/)
A — Yes, breaking changes are likely or certain [URI_PATH, HEADER, QUERY_PARAM]
- B: No, we will maintain strict backwards compatibility → outcome No Versioning / Backwards Compatibility Only → outcome URI Path Versioning (/v1/, /v2/)
B — No, we will maintain strict backwards compatibility [NO_VERSION, URI_PATH]
(No branches defined.)
Q2 — Is this a publicly available API consumed by external third-party developers?
- A: Yes, public or partner-facing API → outcome URI Path Versioning (/v1/, /v2/) → outcome Header Versioning (Accept / Custom Header)
- B: No, internal API consumed by teams within our organisation → outcome URI Path Versioning (/v1/, /v2/) → outcome Header Versioning (Accept / Custom Header) → outcome Query Parameter Versioning (?version=2)
A — Yes, breaking changes are likely or certain [URI_PATH, HEADER, QUERY_PARAM]
- B: No, we will maintain strict backwards compatibility → outcome No Versioning / Backwards Compatibility Only → outcome URI Path Versioning (/v1/, /v2/)
B — No, internal API consumed by teams within our organisation [URI_PATH, HEADER, QUERY_PARAM]
(No branches defined.)
Q3 — Can you control when your clients upgrade to a new API version, or do clients upgrade at their own pace?
- A: We control client upgrades and can coordinate migrations → outcome Header Versioning (Accept / Custom Header) → outcome Query Parameter Versioning (?version=2)
- B: Clients upgrade independently; version selection must be durable and explicit → outcome URI Path Versioning (/v1/, /v2/) → outcome Header Versioning (Accept / Custom Header)
A — Yes, breaking changes are likely or certain [URI_PATH, HEADER, QUERY_PARAM]
- B: No, we will maintain strict backwards compatibility → outcome No Versioning / Backwards Compatibility Only → outcome URI Path Versioning (/v1/, /v2/)
B — Clients upgrade independently; version selection must be durable and explicit [URI_PATH, HEADER]
(No branches defined.)
Q4 — Is this a REST-style API, or are you using GraphQL (or another schema-first protocol)?
- A: REST or REST-like API → outcome URI Path Versioning (/v1/, /v2/) → outcome Header Versioning (Accept / Custom Header) → outcome Query Parameter Versioning (?version=2)
- B: GraphQL, gRPC, or schema-first protocol → outcome No Versioning / Backwards Compatibility Only
A — Yes, breaking changes are likely or certain [URI_PATH, HEADER, QUERY_PARAM]
- B: No, we will maintain strict backwards compatibility → outcome No Versioning / Backwards Compatibility Only → outcome URI Path Versioning (/v1/, /v2/)
B — GraphQL, gRPC, or schema-first protocol [NO_VERSION]
(No branches defined.)
Outcomes
- URI Path Versioning (/v1/, /v2/)
(URI_PATH) - Reached from Q1 (A), Q1 (B), A (B), Q2 (A), Q2 (B), A (B), Q3 (B), A (B), Q4 (A), A (B).
- Header Versioning (Accept / Custom Header)
(HEADER) - Reached from Q1 (A), Q2 (A), Q2 (B), Q3 (A), Q3 (B), Q4 (A).
- Query Parameter Versioning (?version=2)
(QUERY_PARAM) - Reached from Q1 (A), Q2 (B), Q3 (A), Q4 (A).
- No Versioning / Backwards Compatibility Only
(NO_VERSION) - Reached from Q1 (B), A (B), A (B), A (B), Q4 (B), A (B).
Machine-Readable JSON (Canonical Model)
View JSON
{
"_meta": {
"schema": "https://www.drawdecisiontree.com/decision-dag.schema.json",
"source": "https://www.drawdecisiontree.com",
"description": "DrawDecisionTree.com is a free tool for building, sharing, and embedding interactive decision trees. This file is the machine-readable export of a published decision tree. The `dsl` field contains the original source in the Decision DAG DSL; the `dag` schema is documented at the URL in `schema` above.",
"links": {
"interactive": "https://www.drawdecisiontree.com/trees/drawdecisiontree/engineering-api-versioning",
"embed": "https://www.drawdecisiontree.com/embed/path/drawdecisiontree/engineering-api-versioning",
"dsl_reference": "https://www.drawdecisiontree.com/decision-tree-dsl-reference",
"guides": "https://www.drawdecisiontree.com/guides",
"schema_docs": "https://www.drawdecisiontree.com/decision-dag.schema.json",
"author_trees": "https://www.drawdecisiontree.com/trees/drawdecisiontree"
},
"generated_at": "2026-07-03T21:08:41.926Z"
},
"author": {
"handle": "drawdecisiontree",
"first_name": "Andrew",
"last_name": null,
"avatar_url": "1d32d828-b6ca-40ec-bdd7-771fe7b9c36a/avatar-1778531481027.svg",
"display_name": "Andrew"
},
"file": {
"id": "23854095-447f-452c-922c-8578f33dd483",
"name": "How should I version my API?",
"public_slug": "engineering-api-versioning",
"updated_at": "2026-05-12T16:53:43.587978+00:00",
"url": "https://www.drawdecisiontree.com/trees/drawdecisiontree/engineering-api-versioning",
"json_url": "https://www.drawdecisiontree.com/trees/drawdecisiontree/engineering-api-versioning/tree.json",
"dsl_url": "https://www.drawdecisiontree.com/trees/drawdecisiontree/engineering-api-versioning/tree.dag"
},
"meta": {
"description": "Choosing the wrong API versioning strategy creates long-term maintenance pain, breaks client integrations, and can stall product evolution. This elimination tree narrows down the right approach—URI path versioning, header versioning, query parameter versioning, or a no-versioning backwards-compatibility contract—by working through the characteristics of your API, its consumers, and your team's operational constraints.",
"mode": "elimination",
"entry": "Q1",
"tags": [
"engineering",
"api",
"versioning",
"architecture",
"backend"
],
"image": "https://images.unsplash.com/photo-1558494949-ef010cbdcc31?w=1200&q=80"
},
"questions": [
{
"id": "Q1",
"text": "Do you anticipate introducing breaking changes to this API within the next 12 months?"
},
{
"id": "A",
"text": "Yes, breaking changes are likely or certain [URI_PATH, HEADER, QUERY_PARAM]"
},
{
"id": "B",
"text": "No, we will maintain strict backwards compatibility [NO_VERSION, URI_PATH]"
},
{
"id": "Q2",
"text": "Is this a publicly available API consumed by external third-party developers?"
},
{
"id": "A",
"text": "Yes, public or partner-facing API [URI_PATH, HEADER]"
},
{
"id": "B",
"text": "No, internal API consumed by teams within our organisation [URI_PATH, HEADER, QUERY_PARAM]"
},
{
"id": "Q3",
"text": "Can you control when your clients upgrade to a new API version, or do clients upgrade at their own pace?"
},
{
"id": "A",
"text": "We control client upgrades and can coordinate migrations [HEADER, QUERY_PARAM]"
},
{
"id": "B",
"text": "Clients upgrade independently; version selection must be durable and explicit [URI_PATH, HEADER]"
},
{
"id": "Q4",
"text": "Is this a REST-style API, or are you using GraphQL (or another schema-first protocol)?"
},
{
"id": "A",
"text": "REST or REST-like API [URI_PATH, HEADER, QUERY_PARAM]"
},
{
"id": "B",
"text": "GraphQL, gRPC, or schema-first protocol [NO_VERSION]"
}
],
"outcomes": [
{
"id": "URI_PATH",
"label": "URI Path Versioning (/v1/, /v2/)"
},
{
"id": "HEADER",
"label": "Header Versioning (Accept / Custom Header)"
},
{
"id": "QUERY_PARAM",
"label": "Query Parameter Versioning (?version=2)"
},
{
"id": "NO_VERSION",
"label": "No Versioning / Backwards Compatibility Only"
}
],
"dsl": "dag: How should I version my API?\nversion: 1.0.0\nimage: https://images.unsplash.com/photo-1558494949-ef010cbdcc31?w=1200&q=80\ndescription: Choosing the wrong API versioning strategy creates long-term maintenance pain, breaks client integrations, and can stall product evolution. This elimination tree narrows down the right approach—URI path versioning, header versioning, query parameter versioning, or a no-versioning backwards-compatibility contract—by working through the characteristics of your API, its consumers, and your team's operational constraints.\ntags: engineering, api, versioning, architecture, backend\nentry: Q1\nmode: elimination\n\nQ1: Do you anticipate introducing breaking changes to this API within the next 12 months?\n hint: A \"breaking change\" is any change that would cause a correctly written existing client to fail or behave incorrectly: removing or renaming fields, changing data types, altering authentication requirements, or modifying required request parameters. If your API is early-stage and the contract is still being discovered, assume breaking changes will occur. If the API surface is tiny, internal, and stable, a no-versioning contract may be viable.\n A: Yes, breaking changes are likely or certain [URI_PATH, HEADER, QUERY_PARAM]\n B: No, we will maintain strict backwards compatibility [NO_VERSION, URI_PATH]\n\nQ2: Is this a publicly available API consumed by external third-party developers?\n hint: Public APIs face a much harder versioning problem than internal ones: you cannot coordinate a migration with consumers you don't know, and breaking changes can destroy developer trust. Internal APIs benefit from the ability to run multi-version deploys briefly and then coordinate a cut-over with a small number of known teams. GraphQL APIs (regardless of audience) often use a different evolution strategy—see the later question.\n A: Yes, public or partner-facing API [URI_PATH, HEADER]\n B: No, internal API consumed by teams within our organisation [URI_PATH, HEADER, QUERY_PARAM]\n\nQ3: Can you control when your clients upgrade to a new API version, or do clients upgrade at their own pace?\n hint: If you own all clients (e.g., your own mobile apps, your own backend services), you can coordinate upgrades and deprecate old versions on a predictable schedule. If clients upgrade at their own pace—common with public APIs, SDKs, or long-lived embedded devices—you need a versioning scheme that makes version selection explicit and durable. Query parameter versioning is often rejected by purists but works well when clients need to select versions programmatically.\n A: We control client upgrades and can coordinate migrations [HEADER, QUERY_PARAM]\n B: Clients upgrade independently; version selection must be durable and explicit [URI_PATH, HEADER]\n\nQ4: Is this a REST-style API, or are you using GraphQL (or another schema-first protocol)?\n hint: REST APIs benefit from explicit version identifiers in the URI or headers because the resource model changes over time. GraphQL is designed for additive, versionless evolution using schema directives (@deprecated, nullable fields) and query-level flexibility—introducing URI versioning on a GraphQL endpoint is almost always the wrong choice. If you are using gRPC or similar, versioning conventions differ again and may require a separate analysis.\n A: REST or REST-like API [URI_PATH, HEADER, QUERY_PARAM]\n B: GraphQL, gRPC, or schema-first protocol [NO_VERSION]\n\n[URI_PATH]: URI Path Versioning (/v1/, /v2/)\n color: #2563eb\n description: URI path versioning (/v1/resources) is the most visible, most cacheable, and most widely understood versioning strategy—making it the default choice for most public and partner-facing REST APIs. Version the base path, not individual endpoints, and treat each major version as a separate API surface with its own documentation, changelog, and deprecation timeline. Establish a formal deprecation policy before launching v1: specify the minimum notice period (typically 12–18 months for public APIs), communicate via developer portal announcements and response headers (Sunset, Deprecation), and provide a migration guide. Run multiple major versions in parallel only as long as necessary—each live version is a maintenance and security obligation.\n code: ENG_APIv_URI\n\n[HEADER]: Header Versioning (Accept / Custom Header)\n color: #7c3aed\n description: Header-based versioning (via the Accept header, e.g., Accept: application/vnd.myapi.v2+json, or a custom header such as API-Version) keeps URIs clean and is preferred by REST purists who treat the URI as a stable resource identifier. The trade-off is reduced discoverability and cacheability—CDNs and HTTP caches must be configured to vary on the relevant header, and many HTTP debugging tools do not surface custom headers prominently. Document the version header requirement on the very first page of your API reference, provide it in every code sample, and return a clear 400 error with a descriptive message when the header is absent or invalid. Ensure your API gateway and load balancer are configured to route based on the header value before releasing.\n code: ENG_APIv_HEADER\n\n[QUERY_PARAM]: Query Parameter Versioning (?version=2)\n color: #ea580c\n description: Query parameter versioning (?api_version=2 or ?v=2) is the most pragmatic option for internal APIs where simplicity of client implementation matters more than REST purity. It is easy to test in a browser, easy to log, and easy to route at the gateway layer without custom header handling. It is generally inappropriate for public APIs because query parameters are included in server logs and analytics pipelines, version identifiers in query strings feel like second-class citizens to external developers, and the approach is not idiomatic HTTP. For internal use, define a canonical parameter name (agree on it across all your services), document a clear default version for clients that omit the parameter, and automate version deprecation warnings in your API response headers.\n code: ENG_APIv_QUERY\n\n[NO_VERSION]: No Versioning / Backwards Compatibility Only\n color: #16a34a\n description: Committing to a no-versioning, always-backwards-compatible contract is a high discipline but high reward approach—it forces rigorous API design and eliminates client migration overhead entirely. This strategy is most viable for GraphQL APIs (where additive schema evolution is the norm), for truly stable internal utility APIs, or where you have complete control over all consumers and can deploy atomically. The discipline required is strict: use @deprecated annotations or equivalent, never remove or rename fields, only add optional fields, and treat every schema change as a potential client impact. Invest in a robust schema linter and CI gate that detects breaking changes automatically; a human-only review process will eventually let a breaking change through.\n code: ENG_APIv_NONE\n"
}DSL Representation
dag: How should I version my API?
version: 1.0.0
image: https://images.unsplash.com/photo-1558494949-ef010cbdcc31?w=1200&q=80
description: Choosing the wrong API versioning strategy creates long-term maintenance pain, breaks client integrations, and can stall product evolution. This elimination tree narrows down the right approach—URI path versioning, header versioning, query parameter versioning, or a no-versioning backwards-compatibility contract—by working through the characteristics of your API, its consumers, and your team's operational constraints.
tags: engineering, api, versioning, architecture, backend
entry: Q1
mode: elimination
Q1: Do you anticipate introducing breaking changes to this API within the next 12 months?
hint: A "breaking change" is any change that would cause a correctly written existing client to fail or behave incorrectly: removing or renaming fields, changing data types, altering authentication requirements, or modifying required request parameters. If your API is early-stage and the contract is still being discovered, assume breaking changes will occur. If the API surface is tiny, internal, and stable, a no-versioning contract may be viable.
A: Yes, breaking changes are likely or certain [URI_PATH, HEADER, QUERY_PARAM]
B: No, we will maintain strict backwards compatibility [NO_VERSION, URI_PATH]
Q2: Is this a publicly available API consumed by external third-party developers?
hint: Public APIs face a much harder versioning problem than internal ones: you cannot coordinate a migration with consumers you don't know, and breaking changes can destroy developer trust. Internal APIs benefit from the ability to run multi-version deploys briefly and then coordinate a cut-over with a small number of known teams. GraphQL APIs (regardless of audience) often use a different evolution strategy—see the later question.
A: Yes, public or partner-facing API [URI_PATH, HEADER]
B: No, internal API consumed by teams within our organisation [URI_PATH, HEADER, QUERY_PARAM]
Q3: Can you control when your clients upgrade to a new API version, or do clients upgrade at their own pace?
hint: If you own all clients (e.g., your own mobile apps, your own backend services), you can coordinate upgrades and deprecate old versions on a predictable schedule. If clients upgrade at their own pace—common with public APIs, SDKs, or long-lived embedded devices—you need a versioning scheme that makes version selection explicit and durable. Query parameter versioning is often rejected by purists but works well when clients need to select versions programmatically.
A: We control client upgrades and can coordinate migrations [HEADER, QUERY_PARAM]
B: Clients upgrade independently; version selection must be durable and explicit [URI_PATH, HEADER]
Q4: Is this a REST-style API, or are you using GraphQL (or another schema-first protocol)?
hint: REST APIs benefit from explicit version identifiers in the URI or headers because the resource model changes over time. GraphQL is designed for additive, versionless evolution using schema directives (@deprecated, nullable fields) and query-level flexibility—introducing URI versioning on a GraphQL endpoint is almost always the wrong choice. If you are using gRPC or similar, versioning conventions differ again and may require a separate analysis.
A: REST or REST-like API [URI_PATH, HEADER, QUERY_PARAM]
B: GraphQL, gRPC, or schema-first protocol [NO_VERSION]
[URI_PATH]: URI Path Versioning (/v1/, /v2/)
color: #2563eb
description: URI path versioning (/v1/resources) is the most visible, most cacheable, and most widely understood versioning strategy—making it the default choice for most public and partner-facing REST APIs. Version the base path, not individual endpoints, and treat each major version as a separate API surface with its own documentation, changelog, and deprecation timeline. Establish a formal deprecation policy before launching v1: specify the minimum notice period (typically 12–18 months for public APIs), communicate via developer portal announcements and response headers (Sunset, Deprecation), and provide a migration guide. Run multiple major versions in parallel only as long as necessary—each live version is a maintenance and security obligation.
code: ENG_APIv_URI
[HEADER]: Header Versioning (Accept / Custom Header)
color: #7c3aed
description: Header-based versioning (via the Accept header, e.g., Accept: application/vnd.myapi.v2+json, or a custom header such as API-Version) keeps URIs clean and is preferred by REST purists who treat the URI as a stable resource identifier. The trade-off is reduced discoverability and cacheability—CDNs and HTTP caches must be configured to vary on the relevant header, and many HTTP debugging tools do not surface custom headers prominently. Document the version header requirement on the very first page of your API reference, provide it in every code sample, and return a clear 400 error with a descriptive message when the header is absent or invalid. Ensure your API gateway and load balancer are configured to route based on the header value before releasing.
code: ENG_APIv_HEADER
[QUERY_PARAM]: Query Parameter Versioning (?version=2)
color: #ea580c
description: Query parameter versioning (?api_version=2 or ?v=2) is the most pragmatic option for internal APIs where simplicity of client implementation matters more than REST purity. It is easy to test in a browser, easy to log, and easy to route at the gateway layer without custom header handling. It is generally inappropriate for public APIs because query parameters are included in server logs and analytics pipelines, version identifiers in query strings feel like second-class citizens to external developers, and the approach is not idiomatic HTTP. For internal use, define a canonical parameter name (agree on it across all your services), document a clear default version for clients that omit the parameter, and automate version deprecation warnings in your API response headers.
code: ENG_APIv_QUERY
[NO_VERSION]: No Versioning / Backwards Compatibility Only
color: #16a34a
description: Committing to a no-versioning, always-backwards-compatible contract is a high discipline but high reward approach—it forces rigorous API design and eliminates client migration overhead entirely. This strategy is most viable for GraphQL APIs (where additive schema evolution is the norm), for truly stable internal utility APIs, or where you have complete control over all consumers and can deploy atomically. The discipline required is strict: use @deprecated annotations or equivalent, never remove or rename fields, only add optional fields, and treat every schema change as a potential client impact. Invest in a robust schema linter and CI gate that detects breaking changes automatically; a human-only review process will eventually let a breaking change through.
code: ENG_APIv_NONE
Machine Access
- Static JSON:
/trees/drawdecisiontree/engineering-api-versioning/tree.json - Live JSON (SPA):
/json/drawdecisiontree/engineering-api-versioning - Raw DSL:
/trees/drawdecisiontree/engineering-api-versioning/tree.dag - Canonical HTML:
/trees/drawdecisiontree/engineering-api-versioning
Questions in this decision tree
- Do you anticipate introducing breaking changes to this API within the next 12 months?
- Yes, breaking changes are likely or certain [URI_PATH, HEADER, QUERY_PARAM]
- No, we will maintain strict backwards compatibility [NO_VERSION, URI_PATH]
- Is this a publicly available API consumed by external third-party developers?
- Yes, public or partner-facing API [URI_PATH, HEADER]
- No, internal API consumed by teams within our organisation [URI_PATH, HEADER, QUERY_PARAM]
- Can you control when your clients upgrade to a new API version, or do clients upgrade at their own pace?
- We control client upgrades and can coordinate migrations [HEADER, QUERY_PARAM]
- Clients upgrade independently; version selection must be durable and explicit [URI_PATH, HEADER]
- Is this a REST-style API, or are you using GraphQL (or another schema-first protocol)?
- REST or REST-like API [URI_PATH, HEADER, QUERY_PARAM]
- GraphQL, gRPC, or schema-first protocol [NO_VERSION]
Possible outcomes
- URI Path Versioning (/v1/, /v2/)
- Header Versioning (Accept / Custom Header)
- Query Parameter Versioning (?version=2)
- No Versioning / Backwards Compatibility Only
How to use this decision tree
Click "Open interactive version" to step through the questions. Your answers narrow the tree until a recommended outcome is reached. You can also embed this tree on your own site.
More decision trees by Andrew
Which API design pattern is right for my project?
Determine the right API design style for your integration scenario.
CI/CD Pipeline Tool Selection
Choosing a CI/CD platform is a long-term infrastructure commitment — pipelines accumulate config, custom scripts, and team muscle memory that make switching painful. This tree eliminates tools that don't fit your source control host, infrastructure model, or team scale, leaving only the options genuinely viable for your situation.
Which cloud provider should I use — AWS, Azure, or Google Cloud?
Answer a few questions to identify the most suitable cloud platform for your workload.
Container Orchestration Platform Selection
Container orchestration is foundational infrastructure — the platform you choose shapes how you deploy, scale, network, and operate every service you run. This tree eliminates options that don't match your operational maturity, cloud provider commitment, and workload complexity, so you land on the platform that fits your team today without over-engineering for a scale you haven't reached.
How do I assess the health of a customer account?
Classify a customer's health score to guide proactive engagement and retention strategy. Use this tree during your regular account reviews or whenever a trigger event—such as a low NPS, a support spike, or a missed check-in—prompts a reassessment. The outcome drives the cadence and urgency of your next CSM action.
How should I escalate this customer issue?
Determine the appropriate escalation path when a customer issue exceeds normal CSM handling. This tree helps you move quickly and confidently when a situation is deteriorating, ensuring the right people are engaged at the right time. Use it the moment you sense an issue may outgrow your ability to resolve it alone.