Building REST APIs
HTTP Status Codes
আপনি একটি free preview lesson দেখছেন।
আমরা এখন পর্যন্ত define করেছি:
HTTP endpoints
Controllers / Handlers
Request DTOs
Response DTOs
এখন আরেকটি গুরুত্বপূর্ণ contract দরকার:
Request সফল বা ব্যর্থ হলে client কীভাবে বুঝবে কী ঘটেছে?
এর প্রথম signal হলো:
HTTP status code
For example:
201 Created
বা:
404 Not Found
বা:
409 Conflict
Status code API client-কে response body parse করার আগেই broad outcome communicate করে।
এই lesson-এর goal:
আমাদের Product, Inventory, এবং Order APIs-এর expected outcomes-কে consistent HTTP status semantics-এ map করা।
Status Code Is Part of the API Contract
ধরুন client call করে:
GET /orders/O-1001
Response body:
{
"id": "O-1001",
"status": "UNPAID"
}
কিন্তু status:
500 Internal Server Error
হলে body যত ভালোই হোক, contract contradictory।
Similarly:
200 OK
with:
{
"error": "Order not found"
}
also weak design।
Status code এবং response body একসঙ্গে একই outcome communicate করা উচিত।
Broad Status Families
HTTP status codes broadly grouped:
2xx
→ success
4xx
→ request/client/context prevents operation
5xx
→ server failed unexpectedly or infrastructure failed
আমাদের current API-তে primarily এই তিন family গুরুত্বপূর্ণ।
200 OK
200 OK means request successfully processed এবং response body আছে বা থাকতে পারে।
Typical read:
GET /products/P-100
Success:
200 OK
with:
{
"id": "P-100",
"name": "Keyboard",
"price": 100.00
}
Collection Reads
For:
GET /products
or:
GET /orders
successful response normally:
200 OK
even if collection is empty।
Example:
{
"items": []
}
or whatever pagination response shape we later define।
Empty result is not automatically:
404
because the collection resource exists; it just currently contains no matching items।
201 Created
201 Created is appropriate when request successfully creates a new resource।
Examples:
POST /products
and:
POST /orders
On successful creation:
201 Created
This is more precise than generic:
200 OK
because it communicates:
A new resource now exists.
Location With 201
Creation may also return:
Location: /orders/O-1001
This identifies the newly created resource।
Conceptually:
POST /orders
↓
201 Created
Location: /orders/O-1001
Useful and conventional।
204 No Content
204 No Content means:
operation succeeded
no response body
For example, an update endpoint could return:
204 No Content
if client doesn't need updated representation।
But remember:
A
204response must not be treated as if it also contains normal JSON content.
Do We Need 204 Everywhere?
No।
For important business operations such as:
cancel Order
update Product
set Inventory
returning the resulting representation using:
200 OK
may be more useful।
For example:
POST /orders/O-1001/cancel
could return:
200 OK
with:
{
"id": "O-1001",
"status": "CANCELLED",
...
}
That gives client the confirmed final state।
Success Status Should Match Observable Outcome
Useful guideline:
resource created
→ 201
successful read/update with body
→ 200
successful operation intentionally without body
→ 204
Do not choose status purely from habit।
400 Bad Request
400 Bad Request fits requests that fail the API input contract।
Examples:
malformed JSON
missing required request field
invalid field format
quantity has an invalid basic value
invalid path identifier representation
For example:
POST /orders
with:
{
"items": null
}
may result in:
400 Bad Request
Transport Validation vs Business Conflict
Important distinction:
Suppose request:
{
"items": [
{
"productId": "P-100",
"quantity": -5
}
]
}
This clearly fails request validation।
400 is a natural fit।
But suppose quantity is valid:
quantity = 5
while Inventory has:
2
Now request shape is valid।
The operation fails because current business state cannot satisfy it।
That is a different category।
Don't Use 400 for Everything
A weak API maps every expected failure to:
400 Bad Request
Examples:
Order missing
→ 400
Product missing
→ 400
not authenticated
→ 400
not authorized
→ 400
Order already paid
→ 400
This throws away useful semantics।
Clients then have to parse strings to understand what happened।
401 Unauthorized
Despite the confusing name, 401 is primarily about authentication।
Use it when the server cannot establish a valid authenticated caller।
Examples:
missing authentication credential
invalid credential
expired/unacceptable token
Conceptually:
Request
↓
Authentication
↓
identity not established
↓
401
Authentication Happens Before the UseCase
Suppose:
POST /orders
requires authenticated customer identity।
If token is invalid, we should not execute:
CreateOrderUseCase
and then discover customer is missing।
Security boundary rejects request first:
401 Unauthorized
403 Forbidden
403 Forbidden means:
caller is authenticated
but not permitted to perform this operation
Example:
CUSTOMER
attempts:
POST /products
which is admin-only।
Response:
403 Forbidden
is appropriate।
401 vs 403
A useful mental model:
401
→ Who are you? Valid authentication not established.
403
→ We know who you are, but you cannot do this.
Keep those semantics distinct।
Ownership Is More Nuanced
Suppose authenticated customer A requests:
GET /orders/O-2000
but the Order belongs to customer B।
We could return:
403 Forbidden
because A is not authorized।
But this reveals:
Order O-2000 exists.
For customer-owned resources, that may be undesirable।
404 for Hidden Ownership
A common security-oriented approach is:
not found
OR
not visible to this customer
both become:
404 Not Found
Then client cannot distinguish:
resource genuinely absent
from:
resource exists but belongs to someone else
For customer Order access, this is a reasonable v1 direction।
404 Not Found
404 fits when requested resource cannot be found within the caller's accessible resource scope।
Examples:
GET /products/P-999
if Product does not exist or isn't visible according to that endpoint contract।
GET /orders/O-999
if no accessible Order exists।
404 for Mutations Too
404 is not only for GET।
For example:
POST /orders/O-999/cancel
if the accessible Order does not exist:
404 Not Found
is appropriate।
Similarly:
PATCH /products/P-999
for admin could return:
404 Not Found
409 Conflict
409 Conflict is useful when request is structurally valid but conflicts with current resource state or business state।
Our Order lifecycle provides good examples।
Suppose:
Order = PAID
Client requests:
POST /orders/O-1001/cancel
Request itself is well-formed।
Order exists।
Caller owns it।
But lifecycle says:
PAID → CANCELLED
is invalid।
A natural response:
409 Conflict
Another 409 Example
Suppose Order is already:
CANCELLED
and client tries:
POST /orders/O-1001/pay
This conflicts with the current Order state।
Again:
409 Conflict
is a reasonable mapping।
Insufficient Inventory
Suppose client requests:
10 units
but available Inventory is:
3 units
The request shape is valid।
Product exists।
But current application state cannot satisfy it।
This can reasonably map to:
409 Conflict
because the requested operation conflicts with current Inventory state।
Product Inactive
Suppose customer creates an Order containing:
Product P-100
but Product is inactive।
Again:
request structure valid
Product exists
current state does not allow ordering
A conflict-style response is reasonable।
We can map such business-state failures consistently through our error model।
Duplicate Product Lines
What about:
{
"items": [
{
"productId": "P-100",
"quantity": 1
},
{
"productId": "P-100",
"quantity": 2
}
]
}
Our contract explicitly says duplicate Product lines are invalid input։
This is more naturally:
400 Bad Request
than 409, because the submitted request structure itself violates the create-order contract independent of current server state।
This distinction is useful:
invalid request composition
→ 400
valid request conflicts with current business state
→ 409
Product Not Found During Order Creation
Suppose request contains:
P-999
which does not exist।
Possible mappings include:
404
or a request-level business error depending on API design।
For our API, treating referenced missing resources as:
404 Not Found
is reasonable and clear։
The error body can specify which Product reference could not be resolved।
Referenced Resource vs Primary Resource
There is nuance।
Request:
POST /orders
is not itself requesting:
/products/P-999
but the operation references Product P-999।
Still, 404 can communicate:
A resource required by this operation does not exist.
The important thing is consistent documentation।
422 Unprocessable Content?
You may encounter APIs using:
422 Unprocessable Content
for requests that are syntactically valid but semantically invalid।
For example:
quantity <= 0
duplicate Product lines
business validation failures
This can be a valid API style।
But our goal is not to use every available status code।
For this course, a simpler mapping is enough:
malformed/invalid request
→ 400
resource absent
→ 404
current-state conflict
→ 409
This keeps the contract easy to understand।
Don't Debate 400 vs 422 Forever
Teams often spend excessive time debating whether a validation failure should be:
400
or:
422
What matters more:
consistent convention
structured error code
clear documentation
predictable behaviour
A perfectly consistent 400 strategy is often better than random mixtures of 400/409/422 across endpoints।
405 Method Not Allowed
Suppose endpoint supports:
GET /products/P-100
but client sends:
DELETE /products/P-100
and no DELETE mapping exists।
Spring may respond:
405 Method Not Allowed
This is typically framework-level behaviour։
Our Handler doesn't need custom domain logic for it।
415 Unsupported Media Type
If endpoint expects JSON:
Content-Type: application/json
but client sends unsupported content type, framework may return:
415 Unsupported Media Type
Again, primarily transport/framework concern।
We do not manually handle this in every Controller।
5xx Server Errors
5xx should represent unexpected server/infrastructure failures։
Examples:
database unavailable
unexpected NullPointerException
unhandled programming bug
unexpected internal infrastructure failure
For example:
500 Internal Server Error
may be returned for an unexpected exception।
Business Rejection Is Not 500
Suppose:
order.cancel();
throws because Order is already PAID।
This is an expected business rejection։
It should not automatically become:
500 Internal Server Error
just because Java represented it with an exception।
The error mapping layer must distinguish:
expected application/domain failure
from:
unexpected system failure
Payment Provider Failure
Payment introduces more nuance।
Suppose:
PaymentService
calls provider and provider returns a known payment rejection։
That may be an expected business/integration result, not a 500।
But if provider is unavailable due infrastructure failure, we may need a 5xx response such as:
502 Bad Gateway
or:
503 Service Unavailable
depending on the exact failure semantics।
We will design this more carefully in Module 9 when provider behaviour is known।
502 Bad Gateway
For a backend acting as a server to clients and a client to another upstream system, 502 can represent:
The upstream system returned an invalid/unusable failure while handling this request.
It can be relevant for external integrations։
But don't introduce detailed mappings before we know the actual provider contract।
503 Service Unavailable
503 can represent:
service temporarily unable to handle request
For example due to:
dependency unavailable
temporary overload
maintenance
Again, operational policies and retry semantics matter।
We will not use it casually for every exception।
500 Internal Server Error
500 is the generic fallback for unexpected application failure।
Example:
programming bug
unclassified runtime failure
Client should receive a safe error response without:
stack trace
SQL statement
Java internals
while logs retain enough information for engineers to investigate।
Status Code Should Not Leak Internal Architecture
Suppose PostgreSQL throws:
constraint violation
That does not mean client should receive raw database exception semantics։
The application asks:
What does this failure mean at the API boundary?
If it's due to an expected conflict:
409
may be appropriate।
If it indicates an unexpected implementation problem:
500
may be appropriate।
Translate meaning, not exception class names।
Product Endpoint Statuses
Let's define current direction։
GET /products
Success:
200 OK
Even if result is empty।
GET /products/{productId}
Accessible Product exists:
200 OK
Not found/not visible according to endpoint contract:
404 Not Found
POST /products
Valid admin request, Product created:
201 Created
Invalid request:
400 Bad Request
Unauthenticated:
401 Unauthorized
Authenticated but not admin:
403 Forbidden
PATCH /products/{productId}
Success:
200 OK
with updated representation।
Invalid update payload:
400 Bad Request
Product missing:
404 Not Found
Not permitted:
403 Forbidden
POST /products/{productId}/deactivate
Success:
200 OK
Product missing:
404 Not Found
Unauthorized role:
403 Forbidden
If future requirements make repeated deactivation an invalid operation, that could become:
409 Conflict
But our current Product model treats repeated deactivation as harmless/idempotent, so we should not invent a conflict yet।
Inventory Endpoint Statuses
GET /inventory
Admin success:
200 OK
Non-admin:
403 Forbidden
GET /inventory/{productId}
Inventory exists:
200 OK
No Inventory/Product resource:
404 Not Found
PUT /inventory/{productId}
Valid quantity update:
200 OK
or 204 if we intentionally return no body।
For this course, returning updated Inventory representation with 200 is useful।
Negative quantity:
400 Bad Request
Target Product/Inventory not found:
404 Not Found
Customer without admin permission:
403 Forbidden
Order Endpoint Statuses
Now the most important API։
POST /orders
Success:
201 Created
Invalid body:
no items
negative/zero quantity
duplicate Product lines
malformed input
→
400 Bad Request
Missing Product During Order Creation
Example:
P-999 does not exist
A reasonable response:
404 Not Found
Error body can identify the missing required Product reference։
Inactive Product
Product exists but cannot participate in new Order։
This is current-state conflict:
409 Conflict
Insufficient Inventory
Request is valid, but current Inventory cannot satisfy it:
409 Conflict
This communicates:
Retry may become possible if business state changes.
GET /orders
Authenticated customer success:
200 OK
No Orders:
200 OK
with empty collection।
Missing/invalid authentication:
401 Unauthorized
GET /orders/{orderId}
Own accessible Order:
200 OK
No accessible Order:
404 Not Found
This can intentionally cover:
does not exist
and:
belongs to another customer
for customer-facing access if we choose existence-hiding behaviour।
POST /orders/{orderId}/cancel
Eligible own UNPAID Order:
200 OK
with resulting:
CANCELLED
representation।
No accessible Order:
404 Not Found
PAID Order:
409 Conflict
Already CANCELLED Order:
409 Conflict
Unauthenticated:
401 Unauthorized
POST /orders/{orderId}/pay
Eligible own UNPAID Order + successful provider payment:
200 OK
with:
status = PAID
No accessible Order:
404 Not Found
Already PAID:
409 Conflict
CANCELLED:
409 Conflict
Authentication missing:
401 Unauthorized
Provider-specific failure mapping is deferred until we understand the Payment Service contract।
Why Not 400 for Already Paid?
Because the HTTP request is structurally valid।
The conflict is:
current Order state
Therefore:
409 Conflict
communicates the situation better।
Why Not 404 for Inactive Product?
The Product does exist।
The problem is:
it is not currently valid for this operation
So:
409 Conflict
is more expressive than pretending it doesn't exist—
unless the specific customer-facing API intentionally hides inactive Products entirely।
Context matters։
Endpoint Context Can Change the Meaning
Example:
GET /products/P-100
if inactive Products are intentionally invisible to customers, customer-facing read may return:
404 Not Found
But during:
POST /orders
if Product reference was valid but state changed between browse and creation, returning:
409 Conflict
can communicate:
This Product can no longer be ordered.
Same underlying Product state can produce different API semantics depending on operation।
Status Mapping Should Be Centralized
We do not want every Handler to contain:
catch (ProductNotFoundException ex) {
return ResponseEntity.notFound().build();
}
and similar blocks repeatedly।
Instead, expected application/domain errors should be translated in one consistent place।
In Spring, this is commonly implemented with:
@ControllerAdvice
@ExceptionHandler
We will cover consistent error responses later in this module।
Domain Error Does Not Carry HTTP Status
Avoid:
public class OrderNotCancellableException
extends RuntimeException {
private final int statusCode = 409;
}
Domain should express business meaning:
Order cannot be cancelled.
HTTP layer decides:
409 Conflict
This keeps domain transport-independent।
UseCase Should Not Return HTTP Codes
Avoid:
public int execute(...) {
return 404;
}
or:
return HttpStatus.CONFLICT;
Application operation should return:
result
or raise/represent an application/business failure।
Handler/error mapping owns HTTP translation।
Avoid 200 With success=false
Weak:
200 OK
{
"success": false,
"error": "Insufficient inventory"
}
Now HTTP layer says:
success
while body says:
failure
Use:
409 Conflict
plus structured error body।
Avoid Error Strings as the Only Contract
Weak:
{
"message": "Something went wrong"
}
Clients need stable machine-readable information。
Later our error response will likely include a stable error identifier such as:
INSUFFICIENT_INVENTORY
alongside human-readable message।
Status code gives broad HTTP category।
Error code gives application-specific meaning।
Status Code vs Error Code
Example:
409 Conflict
could represent several business conflicts:
ORDER_ALREADY_PAID
ORDER_ALREADY_CANCELLED
INSUFFICIENT_INVENTORY
PRODUCT_NOT_ORDERABLE
So status code alone is intentionally broad।
Structured error body gives specific application meaning।
Don't Encode Business Meaning Only in Status
We should not invent:
one unique HTTP status per domain error
HTTP has a finite standard vocabulary।
Use:
HTTP status
+
application error code
for precision।
Empty Collection vs Missing Resource
Important distinction:
GET /orders
no Orders:
200 OK
empty collection।
But:
GET /orders/O-999
no Order:
404 Not Found
Collection absence and item absence are different semantics।
Update of Missing Resource
Suppose:
PATCH /products/P-999
Body is valid, but Product doesn't exist।
Return:
404 Not Found
Do not silently create the Product unless endpoint contract explicitly defines upsert behaviour।
Our PATCH does not।
PUT Does Not Automatically Mean Upsert
HTTP PUT can sometimes support creation at a client-chosen URI।
But our:
PUT /inventory/{productId}
does not need to mean:
Create Inventory for any arbitrary Product ID.
Our product/application contract can require the Product/Inventory target to exist।
If not:
404 Not Found
Method semantics do not override application rules।
Concurrency Conflicts
Later, if persistence uses optimistic concurrency and two updates conflict, an application may map the resulting conflict to:
409 Conflict
depending on API design։
But we have not selected our exact concurrency strategy yet।
Don't expose version/concurrency error contracts prematurely।
Database Constraint Violations
Suppose a database constraint prevents:
negative Inventory
If this represents a normal expected user/business conflict, translate it into an application-level error where possible।
Do not return raw:
SQLState
constraint name
to client։
Database constraint is a final integrity guard, not public API vocabulary।
Status Codes and Logging
Expected:
400
404
409
are often not production incidents by themselves։
For example customer requesting an old Order ID may legitimately get 404।
Unexpected:
500
is more operationally significant।
Later observability can distinguish:
client/business failures
server failures
rather than treating all non-2xx responses equally।
Status Codes and Metrics
A useful production metric may group responses by:
2xx
4xx
5xx
But high 409 during checkout might still indicate a business/Inventory issue worth investigating।
HTTP status provides useful dimensions, but operational meaning comes from context and application error codes too।
Do Not Hide Bugs as 400
A dangerous pattern:
catch (Exception ex) {
return badRequest();
}
Now:
NullPointerException
database failure
programming bug
all become:
400 Bad Request
This misleads clients and hides production failures।
Only known request/application errors should become controlled 4xx responses।
Unexpected exception → safe 5xx response + internal logging।
Do Not Hide Dependency Failures as 409
Likewise, if PostgreSQL is unavailable, response should not say:
409 Conflict
just because Order creation couldn't complete।
The request did not conflict with business state।
The system failed to execute it।
That's a server/infrastructure failure।
A Practical Mapping Table
For our current API, a useful baseline:
| Situation | HTTP Status |
|---|---|
| Successful read | 200 OK |
| Successful update with response | 200 OK |
| New resource created | 201 Created |
| Success with intentionally no body | 204 No Content |
| Malformed/invalid request | 400 Bad Request |
| Authentication not established | 401 Unauthorized |
| Authenticated but role not permitted | 403 Forbidden |
| Resource not found/not visible | 404 Not Found |
| Valid request conflicts with current business state | 409 Conflict |
| Unexpected application failure | 500 Internal Server Error |
This is our simple v1 foundation।
Product Examples
POST /products
valid admin request
→ 201
POST /products
negative price
→ 400
POST /products
CUSTOMER role
→ 403
PATCH /products/P-999
→ 404
Inventory Examples
PUT /inventory/P-100
quantity = 20
→ 200
PUT /inventory/P-100
quantity = -5
→ 400
PUT /inventory/P-999
target unavailable
→ 404
Order Examples
POST /orders
valid
→ 201
POST /orders
empty items
→ 400
POST /orders
duplicate Product lines
→ 400
POST /orders
Product missing
→ 404
POST /orders
Product inactive
→ 409
POST /orders
Inventory insufficient
→ 409
Order Lifecycle Examples
POST /orders/O-1/cancel
UNPAID own Order
→ 200
POST /orders/O-1/cancel
PAID Order
→ 409
POST /orders/O-1/cancel
already CANCELLED
→ 409
POST /orders/O-1/pay
CANCELLED Order
→ 409
GET /orders/O-1
not accessible to current customer
→ 404
Status Codes Should Be Predictable Across Endpoints
If missing Product means:
404
in one endpoint, avoid mapping similar missing resource situations to:
400
elsewhere without a reason।
If invalid body means:
400
keep that convention consistent।
API predictability reduces client-side special cases।
A Client Should Be Able to Reason Broadly
Client can interpret:
2xx
→ operation succeeded
400
→ fix request input
401
→ establish authentication
403
→ current identity lacks permission
404
→ requested accessible resource unavailable
409
→ refresh/reconsider current business state
5xx
→ server/dependency problem
Application error codes then give precise details।
This is a strong, simple contract।
Common Mistake 1 — Everything Is 200
Clients cannot rely on HTTP semantics।
Common Mistake 2 — Everything Is 400
Malformed requests, missing resources, auth failures, and business conflicts become indistinguishable।
Common Mistake 3 — Every Exception Is 500
Expected domain/application rejections become server failures।
Common Mistake 4 — 401 and 403 Used Interchangeably
Authentication and authorization mean different things।
Common Mistake 5 — Empty Collection Returns 404
An empty collection is usually a valid successful collection response।
Common Mistake 6 — Client-Side Business Conflict Returns 500
For example paid Order cancellation is an expected 4xx business rejection।
Common Mistake 7 — Database Error Leaks Directly
Clients should receive application/API meaning, not SQL implementation details।
Common Mistake 8 — Status Code Encoded in Domain Exception
Domain should remain HTTP-independent।
Common Mistake 9 — Timeout Automatically Means Client Error
Infrastructure/network failure needs its own semantics; it is not automatically 400 or 409।
Common Mistake 10 — Too Many Status Codes Without Consistency
More nuanced does not automatically mean better।
A small predictable status vocabulary is often easier to integrate with।
Status Code Review Checklist
For each API failure, ask:
Did the request fail basic input/transport validation?
→ 400
Was authentication not established?
→ 401
Is the authenticated caller not permitted by role?
→ 403
Is the required accessible resource missing?
→ 404
Is the request valid but current business state
does not permit the operation?
→ 409
Did our application/infrastructure fail unexpectedly?
→ 5xx
For success:
Was a resource created?
→ 201
Is this a normal successful response with body?
→ 200
Is success intentionally body-less?
→ 204
Keep HTTP Mapping at the Edge
Our flow remains:
Domain / UseCase failure
↓
HTTP error mapping
↓
status code + error body
Not:
Order
→ knows 409
or:
Product
→ throws 404
This preserves transport independence।
Engineering Principle
The core principle:
HTTP status codes describe the category of observable outcome; domain and application errors describe the business meaning behind that outcome.
Another:
Use
4xxfor expected request/auth/business failures, and reserve5xxfor genuine server or infrastructure failures.
And:
Consistency is more valuable than using the largest possible vocabulary of HTTP status codes.
Summary
In this lesson, we learned that:
- HTTP status codes are part of the public API contract.
200 OKfits successful reads and updates that return a body.- Empty collection reads should normally still return
200. 201 Createdfits successful Product and Order creation.204 No Contentis useful only when success intentionally has no response body.400 Bad Requestfits malformed or invalid request input.401 Unauthorizedmeans valid authentication was not established.403 Forbiddenmeans the caller is authenticated but lacks permission.- Customer-owned resource existence can be hidden by returning
404for both missing and inaccessible resources. 404 Not Foundalso applies to mutations targeting missing resources.409 Conflictfits valid requests that cannot proceed because of current business state.- PAID Order cancellation, CANCELLED Order payment, inactive Product ordering, and insufficient Inventory are natural conflict cases.
- Duplicate Product lines are better treated as invalid request input than current-state conflict.
- Expected application/domain failures should not become
500. - Unexpected application or infrastructure failures should result in safe
5xxresponses. - External Payment Provider failure semantics will be defined later from the actual integration contract.
- Status codes should remain broad while structured application error codes provide precise business meaning.
- Domain objects and UseCases should not know HTTP status codes.
- HTTP error translation should be centralized rather than repeated in every Handler.
- A small, consistent status vocabulary is preferable to ad hoc status selection.
Next lesson:
Request Validation
There we will add validation to our Product, Inventory, and Order request DTOs using Spring/Jakarta Bean Validation, distinguish transport validation from domain invariants, and decide exactly where checks such as required fields, positive quantity, duplicate Products, Product existence, and Inventory availability belong.