Building REST APIs

API Versioning

ReadingPreview

আপনি একটি free preview lesson দেখছেন।

আমাদের API routes এখন পর্যন্ত এমন:

/api/v1/products

/api/v1/inventory

/api/v1/orders

এখানে:

/api

বলে এটি application API,

আর:

/v1

বলে client কোন public API contract ব্যবহার করছে।

কিন্তু API versioning নিয়ে একটি common misconception হলো:

Backend code change হলেই নতুন API version দরকার।

এটি ঠিক নয়।

আমরা internal implementation পুরোপুরি বদলে ফেলতে পারি:

Repository implementation

database schema

domain structure

package layout

query strategy

তবুও যদি client-visible contract একই থাকে, API version change করার কোনো কারণ নেই।

এই lesson-এর goal:

API version-কে implementation version না বানিয়ে public contract compatibility manage করার mechanism হিসেবে ব্যবহার করা।


What Are We Versioning?

Consider:

GET /api/v1/orders/O-1001

Client cares about things such as:

HTTP method

URI

request parameters

request body

response structure

status codes

error codes

field meanings

Client does not care whether backend internally uses:

JPA

plain JDBC

different Repository implementation

different package structure

So:

API version represents the public contract, not the internal codebase version.


Why Version APIs?

Suppose frontend expects:

{
  "id": "O-1001",
  "status": "UNPAID",
  "total": 100.00
}

Later backend changes response to:

{
  "orderId": "O-1001",
  "state": "PENDING_PAYMENT",
  "amount": 100.00
}

Old frontend now looks for:

id

status

total

and they no longer exist।

The server may work perfectly।

But the contract is incompatible।

API versioning gives us a way to introduce intentionally incompatible contracts without silently breaking existing consumers।


Our Versioning Strategy

For this course, we will use URI-based major versioning:

/api/v1/...

Examples:

GET /api/v1/products
POST /api/v1/orders
POST /api/v1/orders/{orderId}/cancel

This approach is simple, explicit, and easy to understand।


Why Version in the URI?

There are several possible approaches:

URI versioning

header versioning

media-type versioning

query-parameter versioning

For example, some APIs might use:

Accept: application/vnd.example.v1+json

or:

GET /orders?version=1

Those approaches can work।

But they introduce additional concepts that our application does not need।

For our course:

/api/v1

keeps version selection:

visible

easy to document

easy to route

easy to test

Version Only the Major Contract

We are not going to expose routes such as:

/api/v1.2.7/orders

or:

/api/v1.4/products

Every backward-compatible feature does not need a new URI version।

Think of:

v1

as a major contract family।

Within v1, compatible additions and fixes can continue to evolve।


What Is a Breaking Change?

A breaking change is one that can cause an existing valid API consumer to stop working or behave incorrectly।

Examples include:

removing a response field

renaming a response field

changing a field type

removing an endpoint

changing an HTTP method

changing the meaning of an existing field

making previously optional input required

removing an accepted enum value

changing success/failure semantics incompatibly

These changes require careful compatibility planning।


Example: Renaming a Response Field

v1:

{
  "id": "P-100",
  "price": 100.00
}

Suppose we change:

price

to:

unitPrice

Result:

{
  "id": "P-100",
  "unitPrice": 100.00
}

An existing client doing:

product.price

now fails।

This is a breaking contract change।


Internal Rename Is Different

Suppose Java changes:

private BigDecimal price;

to:

private BigDecimal currentPrice;

but ProductResponse still maps it to:

{
  "price": 100.00
}

Public contract did not change।

No API version change needed।

This is one of the benefits of explicit DTO mapping।


Removing a Field Is Usually Breaking

Suppose v1 Order response:

{
  "id": "O-1001",
  "status": "UNPAID",
  "total": 100.00
}

Removing:

total

can break consumers that depend on it।

So removing public fields requires deliberate versioning or migration planning।


Is Adding a Field Breaking?

Often, adding an optional response field is backward-compatible।

Suppose:

{
  "id": "O-1001",
  "status": "UNPAID",
  "total": 100.00,
  "createdAt": "2026-08-09T18:30:00Z"
}

Existing clients that ignore unknown JSON fields normally continue to work।

So adding:

createdAt

is often compatible।

But "often" matters।

If a client uses a strict schema that rejects unknown fields, even additive changes can cause problems।

A well-designed JSON consumer should generally tolerate additional response fields।


Adding a New Endpoint Is Usually Compatible

Suppose v1 currently supports:

GET /api/v1/products

POST /api/v1/orders

Later we add:

GET /api/v1/orders/{orderId}

Existing consumers are unaffected।

So we can add the endpoint under:

/api/v1

without introducing v2।


Adding an Optional Request Field

Suppose:

{
  "name": "Keyboard",
  "price": 100.00
}

later also supports an optional field:

{
  "name": "Keyboard",
  "price": 100.00,
  "description": "..."
}

Old clients don't send it।

If server continues accepting the old request, this can remain backward-compatible।

No new version required।


Adding a Required Request Field Is Different

Suppose v1 clients send:

{
  "name": "Keyboard",
  "price": 100.00
}

Then we change the API so that:

category

is mandatory:

{
  "name": "Keyboard",
  "price": 100.00,
  "category": "ACCESSORIES"
}

Existing requests are now invalid।

That is breaking।

We should not silently change v1 like this without a compatibility strategy।


Changing a Field Type Is Breaking

Suppose:

{
  "price": 100.00
}

becomes:

{
  "price": {
    "amount": 100.00,
    "currency": "EUR"
  }
}

Even if the new representation is "better," existing clients expect a number।

This is a breaking change।

A future major API version may be appropriate if such a redesign becomes necessary।


Enum Changes Need Care

Current Order status:

UNPAID

PAID

CANCELLED

Suppose we add:

REFUNDED

Is that backward-compatible?

Not automatically।

Some clients may write:

switch (order.status) {
    case "UNPAID":
        ...
        break;
    case "PAID":
        ...
        break;
    case "CANCELLED":
        ...
        break;
}

and assume no other value exists।

A new enum value can therefore affect client behaviour।


Design Clients for Extensible Enums

When possible, clients should handle:

unknown future enum value

gracefully।

But server teams still need to treat public enum expansion carefully։

Especially when the new value changes workflow assumptions।


Don't Version for Internal Database Changes

Suppose we change from:

orders table

to a redesigned schema with additional tables।

Or add:

indexes

constraints

new columns

If API behaviour stays the same:

/api/v1

remains unchanged।

Database migration version and API version are different concepts।


Flyway Version Is Not API Version

Later we may have migrations such as:

V1__create_products.sql

V2__create_inventory.sql

V3__create_orders.sql

Those numbers do not mean:

API v1

API v2

API v3

Flyway versioning tracks:

database schema evolution

API versioning tracks:

public client contract evolution

Do not couple them।


Application Release Version Is Also Different

We might deploy:

order-management 1.8.4

while still serving:

/api/v1

That's completely normal।

One API major version may survive hundreds of production deployments।


Endpoint Change Example

Suppose v1 cancellation uses:

POST /api/v1/orders/{orderId}/cancel

Later an engineer prefers:

PATCH /api/v1/orders/{orderId}

with:

{
  "status": "CANCELLED"
}

This is not merely implementation refactoring।

It changes the public operation contract।

Existing clients still call:

POST /cancel

So replacing the endpoint would be breaking।


"Cleaner Design" Does Not Justify Breaking Clients

Once an API is public or consumed by another application, client compatibility becomes part of engineering quality।

You may later realize:

We could design this endpoint more elegantly.

That does not mean you casually change v1।

Options include:

keep the old contract

add a compatible alternative

deprecate gradually

introduce a future major version

depending on cost and usage।


Breaking Error Contracts

Our canonical error:

{
  "type": "https://api.liveklass.io/problems/order-not-found",
  "title": "Order not found",
  "status": 404,
  "detail": "The requested order could not be found.",
  "instance": "/api/v1/orders/O-1001",
  "code": "ORDER_NOT_FOUND"
}

If clients depend on:

code

then changing:

ORDER_NOT_FOUND

to:

ORDER_MISSING

is a public contract change।

Error codes need the same stability as successful response fields।


Error Message Text Can Be Less Stable

Changing:

"The requested order could not be found."

to:

"The order could not be found."

should normally not break clients।

Why?

Because clients are expected to use:

code = ORDER_NOT_FOUND

for logic।

This is exactly why machine-readable codes matter।


Problem type Is Also Public

Similarly:

https://api.liveklass.io/problems/order-not-found

should be treated as a stable identifier।

Do not casually rename it just because internal terminology changes।


Compatible Fixes Do Not Need New Versions

Suppose:

POST /orders

incorrectly calculated Order total due a bug।

We fix the bug so the endpoint now follows its documented contract।

That does not require:

/api/v2

It is a correctness fix within v1।

Versioning should not freeze bugs forever।


But Behavioural Fixes Need Judgment

Suppose clients accidentally came to depend on undocumented incorrect behaviour।

Technically fixing it may still break those clients।

Production API changes require:

usage analysis

communication

tests

rollout planning

not purely semantic arguments։

This is part of real-world engineering judgment।


Tightening Validation Can Be Breaking

Suppose v1 accepts:

name = 500 characters

because no maximum existed।

Later we suddenly add:

maximum 100

Existing valid requests may now fail।

Even though this looks like "validation improvement," it changes the accepted input contract।

Therefore validation changes also require compatibility thinking।


Relaxing Validation Is Usually Safer

Suppose v1 requires:

price > 0

and later changes to:

price >= 0

Existing valid requests remain valid।

This is generally backward-compatible։

Of course, business semantics still need to support the new values correctly।


Changing Status Codes Can Be Breaking

Suppose an endpoint previously returns:

409

for ORDER_NOT_CANCELLABLE

Clients may handle:

409

specially।

Changing it to:

400

without reason can break client behaviour।

HTTP status codes are part of the contract too।


Adding Pagination to an Unbounded Endpoint

Suppose old:

GET /orders

returned:

[
  {...},
  {...}
]

Then we change it to:

{
  "items": [...],
  "page": 0,
  "size": 20,
  "totalItems": 100,
  "totalPages": 5
}

That changes response shape completely।

If the endpoint already has consumers, this is breaking।

This is why we design bounded pagination before publishing the endpoint contract।


Good Initial Design Reduces Future Versioning

Careful v1 design includes:

bounded collection endpoints

stable error structure

server-controlled authority

explicit business actions

clear DTOs

intentional enum values

These choices reduce the number of reasons we later need v2।

API versioning is not a substitute for good v1 design।


Do Not Create v2 Too Quickly

A common anti-pattern:

/api/v1/orders

/api/v2/orders

/api/v3/orders

within a short time because every design iteration creates a new version।

This leaves:

multiple code paths

duplicate documentation

more tests

migration burden

security maintenance

client confusion

Major versions are expensive।

Use them deliberately।


One Endpoint Doesn't Need Its Own Version Number

Avoid:

/api/v1/products

/api/v2/orders

/api/v3/inventory

unless the application truly supports independently versioned API families for a strong reason।

For our application:

/api/v1

is a consistent API contract boundary across capabilities।


Where Should the Version Prefix Live?

Conceptually, our route prefix is:

/api/v1

Then capability routes:

/products

/inventory

/orders

combine into:

/api/v1/products

/api/v1/inventory

/api/v1/orders

Spring Mapping

A Controller could conceptually use:

@RestController
@RequestMapping("/api/v1/orders")
public class OrderHandler {
}

This works।

But repeating:

/api/v1

in every Handler may eventually become noisy।


Central Route Constants

A project can define a shared technical constant such as:

public final class ApiRoutes {

    public static final String API_PREFIX =
            "/api/v1";

    private ApiRoutes() {
    }
}

Then:

@RequestMapping(
        ApiRoutes.API_PREFIX + "/orders"
)

This may provide consistency।

But don't create elaborate routing frameworks for three Controllers։

A simple shared prefix constant is enough if repetition becomes useful to centralize।


Version Prefix Is a Transport Concern

Do not put:

v1

inside:

Order

Product

CreateOrderUseCase

The domain does not know which API version invoked it।

Potential future:

/api/v1/orders

and:

/api/v2/orders

might even call the same underlying UseCase when their business behaviour overlaps।

Versioning belongs to the HTTP boundary।


v1 and v2 Can Share Domain Logic

Imagine future v2 changes Order response shape while Order business behaviour remains the same।

Then:

V1 OrderHandler
       ↓
CreateOrderUseCase

and:

V2 OrderHandler
       ↓
CreateOrderUseCase

could potentially share the same application workflow।

Only:

request mapping

response mapping

public contract

may differ।

This is another reason to keep UseCases transport-independent।


Do Not Duplicate the Whole Application for v2

Bad:

v1/
├── domain/
├── repository/
└── usecase/

v2/
├── domain/
├── repository/
└── usecase/

merely because response JSON changed।

Version the boundary that changed।

Share internal behaviour where semantics remain the same।


When Business Meaning Changes

Sometimes a new version changes more than transport shape։

For example, if future Order creation semantics become fundamentally different, v2 may require different application workflow too।

That's okay।

But even then, duplication should follow genuine behavioural divergence, not version-number ceremony।


Deprecation

Suppose eventually:

/api/v2

is introduced।

We may continue serving:

/api/v1

for some transition period।

During that period:

v1
→ supported but deprecated

v2
→ recommended

Clients need time to migrate।


Deprecation Is a Product/Operational Decision

How long v1 remains supported depends on:

number of consumers

control over clients

migration complexity

security considerations

business commitments

There is no universal:

30 days

or:

6 months

rule for every API।


Internal API vs Public API

Even if LiveKlass frontend is currently the only consumer, compatibility still matters।

Why?

Frontend and backend deployments may not happen simultaneously।

For example:

old frontend
+
new backend

may briefly coexist during rollout।

Stable API contracts make independent deployment safer।


Mobile Clients Make Compatibility Even Harder

If a future LiveKlass mobile app consumes the API, users may keep old versions installed for months।

Then backend cannot assume:

every client upgrades immediately

Versioning and backward compatibility become even more important।

This is another reason to develop good habits now।


Contract Tests

API tests should protect public contracts।

For example:

POST /api/v1/orders
→ 201
→ response contains id/status/items/total

Error test:

invalid request
→ 400
→ code = VALIDATION_ERROR

These tests make accidental contract breaks visible during refactoring।


OpenAPI Will Help

In the next lesson, we'll document our API with OpenAPI।

That specification can capture:

paths

methods

request schemas

response schemas

status codes

error responses

It becomes another important source for reviewing whether an API change is backward-compatible।


Versioning OpenAPI Documents

A v1 API specification should describe:

/api/v1/...

If v2 eventually exists, it may have its own API contract/specification।

Don't mix incompatible schemas under the same version while pretending clients can safely use either։


Breaking Change Review Checklist

Before changing a published v1 contract, ask:

Will an existing valid request become invalid?

Will an existing response field disappear?

Will a field be renamed?

Will its type change?

Will an enum gain/change semantics that clients
may not understand?

Will an HTTP status change?

Will an application error code change?

Will endpoint path or method change?

Will pagination shape change?

Will the meaning of an existing field change?

If yes, compatibility deserves explicit review।


Changes That Usually Do Not Need v2

Examples:

fixing a bug

adding database indexes

changing repository implementation

changing JPA mappings internally

refactoring packages

improving logging

adding metrics

optimizing SQL

adding a backward-compatible endpoint

adding an optional request capability

adding a response field where clients tolerate it

These are not automatically major API changes।


Changes That May Require v2

Examples:

renaming core fields

changing request structure incompatibly

changing response structure incompatibly

removing endpoints

changing fundamental resource semantics

changing identifier representation incompatibly

replacing a command endpoint with another contract

making optional data mandatory

major money representation redesign

The word:

may

matters।

Sometimes migration techniques allow compatibility without a new major version।


Compatibility Before Versioning

Suppose we need to rename:

price

to:

unitPrice

Could v1 temporarily return both?

{
  "price": 100.00,
  "unitPrice": 100.00
}

Potentially।

Then clients migrate before old field is removed।

Whether this is worth doing depends on context।

The lesson:

New major version is one compatibility tool, not the only one.


Avoid Permanent Duplicate Fields

Compatibility bridges should have a plan।

Otherwise the API accumulates:

price

unitPrice

newPrice

currentPrice

forever।

If temporary compatibility fields are introduced, document:

which field is preferred

which is deprecated

migration timeline

when relevant।


API Version Is Not Feature Version

Suppose we add Payment support to an existing v1 API:

POST /api/v1/orders/{orderId}/pay

If this is purely additive and does not break existing consumers:

v1 remains v1

A new feature does not automatically require:

v2

API Version Is Not Domain Version

Order may evolve internally from:

3 fields

to:

10 fields

while response exposes only the stable subset।

Again:

domain evolution
≠
API major version

Versioning and Error Format

Our canonical v1 error response:

{
  "type": "https://api.liveklass.io/problems/validation-error",
  "title": "Validation failed",
  "status": 400,
  "detail": "One or more fields are invalid.",
  "instance": "/api/v1/orders",
  "code": "VALIDATION_ERROR",
  "errors": []
}

is part of the v1 contract too।

Changing it to:

{
  "errorCode": "...",
  "errorMessage": "..."
}

would be a major response-contract redesign।

Treat error contracts with the same care as success contracts।


Consistency Across v1

All v1 capabilities should follow the same API conventions:

/api/v1

JSON request/response

canonical Problem Details errors

page/size pagination

server-controlled ownership

server-controlled Order pricing and lifecycle

This creates a coherent API rather than a collection of independently designed endpoints।


A Practical Versioning Policy for This Project

Our current policy:

1. Public HTTP routes use /api/v1.

2. v1 represents the current major API contract.

3. Backward-compatible additions remain in v1.

4. Internal refactoring does not change API version.

5. Breaking changes require explicit compatibility review.

6. We do not create v2 merely because implementation changes.

7. Domain and UseCases remain unaware of API version.

8. Error codes and problem types are versioned contract elements too.

9. New major versions are introduced only when preserving the
   existing contract is no longer practical or desirable.

This is enough for our current system।


Common Mistake 1 — New Version for Every Feature

Adding an endpoint does not automatically require v2।


Common Mistake 2 — New Version for Database Migration

Flyway/schema versions and API versions are different concepts।


Common Mistake 3 — New Version for Refactoring

Clients do not care whether we changed Repository implementation।


Common Mistake 4 — Breaking v1 Because "Frontend Can Update Too"

Independent deployment and future consumers make compatibility valuable even when one team owns both sides।


Common Mistake 5 — Renaming Fields Casually

Response field names are public contract once clients depend on them।


Common Mistake 6 — Ignoring Error Codes During Compatibility Review

ORDER_NOT_FOUND is as much a public identifier as many response fields।


Common Mistake 7 — v1 Domain and v2 Domain Duplicated Automatically

Version the changed boundary, not the whole codebase।


Common Mistake 8 — Version Number in UseCases

UseCases represent application behaviour, not HTTP versions।


Common Mistake 9 — Making Optional Input Required Without Review

Validation changes can be breaking changes too।


Common Mistake 10 — Major Versions Become Cheap

Supporting multiple major versions creates real long-term cost।

Treat version changes as deliberate architecture/product decisions।


API Versioning Review Checklist

Before introducing a new API version, ask:

What specific existing contract is incompatible?

Can the change be additive instead?

Can old and new fields coexist temporarily?

Can the transport mapping change while the same
UseCase/domain remains?

Which existing clients use the current contract?

How will they migrate?

How long must the old contract remain supported?

Are we versioning because of client compatibility,
or merely because the implementation changed?

If the answer is:

"implementation changed"

then a new API version is probably unnecessary।


Engineering Principle

The core principle:

Version the contract clients depend on, not the implementation clients cannot see.

Another:

A major API version is a compatibility boundary, not a release number.

And:

The best versioning strategy is partly avoiding unnecessary breaking changes through deliberate API design from the beginning.


Summary

In this lesson, we learned that:

  • Our API uses URI-based major versioning through /api/v1.
  • API versioning represents the public HTTP contract, not the Java application version.
  • Database migration versions, application release versions, and API versions are independent concepts.
  • Internal refactoring does not require a new API version when observable behaviour remains compatible.
  • Renaming/removing response fields is generally breaking.
  • Changing field types can be breaking.
  • Making previously optional request fields mandatory is breaking.
  • Adding new endpoints is generally backward-compatible.
  • Adding optional request capabilities can often remain in v1.
  • Adding response fields is often compatible when clients tolerate unknown fields, but still deserves deliberate review.
  • Public enum changes can affect existing clients and require care.
  • HTTP status codes, application error codes, and problem type identifiers are also part of the API contract.
  • Fixing a bug according to the documented contract does not automatically require v2.
  • Pagination should be designed before publishing collection endpoints because changing an unbounded array into a page object later can be breaking.
  • Major versions are operationally expensive and should not be created casually.
  • /api/v1 should be consistent across Product, Inventory, and Order capabilities.
  • API version belongs at the HTTP boundary; domain objects and UseCases should remain version-independent.
  • Future v1 and v2 Handlers may share the same UseCases when only transport contracts differ.
  • Backward compatibility, deprecation, and client migration require deliberate engineering judgment.
  • OpenAPI documentation will help us make the v1 contract explicit and review future compatibility changes.

Next lesson:

Documenting APIs with OpenAPI

There we will document the complete v1 API contract—paths, request/response schemas, pagination, status codes, authentication requirements, and our canonical LiveKlass Problem Details error format—and distinguish API documentation from implementation code.