Draft / Scheduled Content
This article is a draft or scheduled for future publication. The content is subject to change.
Your API Doesn't Need Versioning
The Default Version Prefix
When you design a new REST API, it is almost a reflex to prefix your endpoints with a version number:
/api/v1/users/api/v1/orders
We do this because every textbook and blog post tells us it is a best practice. We want to be ready for the future. We think: “When we need to make a breaking change later, we will just create /api/v2/ and migrate our clients smoothly.”
It sounds like a clean, professional engineering strategy.
But in practice, route-based API versioning is an operational bottleneck. It leads to massive code duplication, complex routing layers, and a portfolio of legacy endpoints that you can never delete because clients refuse to migrate.
For the vast majority of web applications, you don’t need API versioning. You need API evolution.
The Cost of the /v2 Split
What actually happens when you deploy /api/v2/?
Suppose you need to change a single endpoint: /api/v1/users returns the user’s name as a single string, but you want to split it into firstName and lastName in /api/v2/users.
Because you changed the version prefix, you cannot just duplicate the user route. You must duplicate the entire routing table under /v2/.
Even if the other 50 endpoints in your application (/orders, /products, /categories) didn’t change, they must now be accessible via /v2/ so the client doesn’t have to mix and match versions.
You are forced to:
- Copy-paste your route handlers.
- Route both
/v1/and/v2/to the same backend services, mapping parameters where necessary. - Maintain twice as many tests.
- Support two code paths in your controllers.
You have doubled your API surface area to make a single database column modification.
+--------------------------------------------+
| API Gateway |
+--------------------------------------------+
/ \
v v
/api/v1/ (Legacy) /api/v2/ (Active)
- /users (returns "name") - /users (splits "name")
- /orders (identical to v2) - /orders (identical to v1)
- /products (identical to v2) - /products (identical to v1)
The Client Migration Paralysis
The promise of versioning is that it allows you to deprecate old code. You tell your clients: “We are releasing v2. You have six months to migrate off v1 before we shut it down.”
This works if you are Stripe or GitHub, and your clients are external developers who read your API changelogs.
But if you are building a typical SaaS or internal product, your clients are your own mobile app developers, your own frontend team, or a handful of close integration partners.
When you tell them they need to migrate, they look at their Scrum board. They have product features to ship, bug fixes to deploy, and deadlines to meet. Migrating to /v2/ is seen as low-priority work because it adds zero visible value to their users.
So, they don’t migrate.
Six months passes. You cannot shut down /v1/ because it would crash your iOS app (which users haven’t updated in two years) or break your biggest enterprise client’s integration.
You end up maintaining /v1/ and /v2/ indefinitely. You are paying a daily tax to support old code paths because you made it too easy to fork your API.
The Alternative: Evolutionary API Design
Instead of versioning your routes, you should design your API to be evolutionary and backward-compatible.
An evolutionary API never breaks existing clients. It only grows.
Here are the rules of evolutionary API design:
1. Never Rename or Delete Fields
If you need to change a field structure, keep the old field and add the new one next to it.
Returning to our user name example:
- Initial State:
{ "name": "Alice Smith" } - Evolved State:
{ "name": "Alice Smith", "first_name": "Alice", "last_name": "Smith" }
Old clients continue to read name and function perfectly. New clients read first_name and last_name. The server calculates name dynamically in the serializer (first_name + ' ' + last_name).
Yes, your payload contains a little redundancy. But JSON is cheap. Duplicating code paths is expensive.
2. Never Make Optional Fields Required
If you add a new parameter to a POST request, make it optional. Give it a sensible default value on the server if the client doesn’t send it.
3. Let Clients Select Their Payload (GraphQL / JSON-LD)
If you want to prevent payload bloat as your API grows, let the client specify what fields they want.
This is the core strength of GraphQL or simple query parameters (like /users?fields=id,email). The client gets exactly what they need, and the server can add new fields to the schema without affecting existing queries.
The Stripe Approach: Header-Based Date Versioning
If you genuinely reach a scale where breaking changes are unavoidable (e.g., you are a public API platform with thousands of third-party integrations), do not version your routes.
Use Header-Based Date Versioning, popularized by Stripe.
The client sends a header indicating the API version they expect:
Stripe-Version: 2026-06-24
Internally, the server runs a single, master controller code path (always the latest version). If the client requested an older version, the server passes the response through a pipeline of compatibility middlewares (transforms) that translate the new data structure back to the old one.
[Latest Database/Code] -> [Transforms (2026 -> 2025 -> 2024)] -> [v2024 Output to Client]
This way, your core application code remains clean and unified. The compatibility logic lives in a separate, isolated layer that can be easily tested and eventually removed.
Evolve, Don’t Fork
Route versioning is an easy way to push today’s database refactoring problems into tomorrow’s integration problems. It encourages developers to write breaking changes because they think /v2/ will save them.
It won’t. It will just double your maintenance burden.
Design your API to grow gracefully. Respect your clients. Keep your endpoints stable.
Related Content
Why MongoDB is Still the Wrong Choice for 99% of Projects
The 'schemaless' pitch of document databases promised database flexibility and rapid iteration. In reality, your data always has schema. Moving schema constraints to the application layer leads to data drift, write corruption, and slow queries. PostgreSQL is almost always the correct answer.
Rust is Great, but You're Probably Using it for the Wrong Reasons
Rust is the darling of the systems programming world, boasting safety and performance without a garbage collector. But rewriting your company's simple HTTP CRUD API in Rust is likely a waste of time and money. Here is why the productivity hit of Rust is rarely worth it for typical web applications.
Stop Sending Nulls in Your API Responses
Why omitting null fields and empty arrays makes your REST APIs faster, cheaper, and cleaner. Personal opinions, not gospel.