Building REST APIs
API Versioning
আপনি একটি 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
typeidentifiers 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/v1should 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.