# API Versioning: The Strategy I Use to Not Break Client Apps
By Govind Bajaj · 2025

Tags: api-design, versioning, backend, rest-api, software-engineering
I changed a response field name and broke 3 client apps. Never again. Here's the URL versioning strategy that lets me evolve APIs without breaking existing clients.
I changed a response field name from totalAmount to orderTotal and broke three client apps.

Two mobile apps and one third-party integration started failing. Support tickets poured in. I had to hotfix the API to support both field names while clients updated.

Never again. Here is the URL versioning strategy that lets me evolve APIs without breaking existing clients.

## Why URL Versioning Over Header Versioning

There are three common API versioning strategies:

URL versioning: /api/v1/orders, /api/v2/orders. The version is explicit and visible. Cache-friendly. Easy to test — just change the URL.

Header versioning: Send API-Version: 2 in headers. Cleaner URLs but harder to test. Requires custom headers in every request.

Media type versioning: Content-Type: application/vnd.api.v2+json. Academically correct. Practically annoying.

I use URL versioning. It is explicit, discoverable, and works with every HTTP client without configuration.

## The Versioning Rules

Rule 1: New versions are additive, not destructive. V2 includes everything in V1 plus new fields or endpoints. Old fields stay.

Rule 2: Deprecated fields are marked, not removed.

```json
{
  "orderTotal": 1500,
  "totalAmount": 1500
}
```

Both fields exist. The old one is marked deprecated in documentation. Clients have six months to migrate.

Rule 3: Breaking changes require a new version. Changing a field type, removing a field, or changing authentication are all breaking changes. They get a new version.

Rule 4: Old versions are supported for at least six months. Clients need time to migrate. Dropping support too quickly forces emergency updates.

## Implementation in Express

```ts
// app.ts
import express from "express"
import v1Routes from "./routes/v1"
import v2Routes from "./routes/v2"

const app = express()

app.use("/api/v1", v1Routes)
app.use("/api/v2", v2Routes)

// Redirect /api to latest version
app.use("/api", (req, res) => {
  res.redirect(307, `/api/v2${req.path}`)
})
```

Each version is a separate route module. Shared logic goes in a services layer. Version-specific logic stays in the route handler.

```ts
// routes/v2/orders.ts
import { getOrders } from "@/services/orders"

router.get("/", async (req, res) => {
  const orders = await getOrders(req.query)
  
  // V2 adds lineItems to each order
  const withItems = await Promise.all(
    orders.map(async (order) => ({
      ...order.toObject(),
      lineItems: await getLineItems(order._id),
      orderTotal: order.totalAmount, // New preferred name
      totalAmount: order.totalAmount, // Deprecated, still present
    }))
  )
  
  res.json(withItems)
})
```

## Handling Shared Code

Business logic is version-agnostic and lives in the services layer.

```ts
// services/orders.ts — shared between v1 and v2
export async function getOrders(query: OrderQuery) {
  return Order.find(query).limit(100).sort({ createdAt: -1 })
}
```

Response formatting is version-specific and lives in the route handler. This separation means business logic changes apply to all versions, while response format changes are isolated.

## Deprecation Communication

When a version is deprecated, communicate it everywhere:

Response headers: Deprecation: true, Sunset: Sat, 01 Jul 2026 00:00:00 GMT

Documentation: Mark deprecated versions clearly. Provide migration guides.

Email: Notify active API consumers at 3 months, 1 month, and 1 week before sunset.

## Takeaways

- URL versioning is explicit, cache-friendly, and works with every HTTP client
- New versions are additive — old fields stay, new fields are added
- Mark deprecated fields but do not remove them immediately
- Separate business logic (shared) from response formatting (version-specific)
- Support old versions for at least six months
- Communicate deprecation through headers, docs, and direct contact
- The goal is evolution without breakage — your clients should never notice an API update