|
Voiced by Amazon Polly |
Overview
A REST API is easy to build when everything goes as expected. A client sends a request, the backend processes it, and the server returns a response.
The real challenge begins when things go wrong.
What happens when a client sends invalid data? What if the requested resource doesn’t exist? What if the same resource already exists? How should the API communicate an unexpected server failure?
A good REST API is not just about returning the right data. It should also make failures clear, consistent, and predictable.
This article examines practical principles for designing REST APIs that are easier to consume, test, and maintain.
Pioneers in Cloud Consulting & Migration Services
- Reduced infrastructural costs
- Accelerated application deployment
REST APIs
- Validate Requests at the Backend
Consider a product creation request:
{
“name”: “Laptop”,
“price”: -50000,
“stockQuantity”: -10
}
The JSON is valid, but the values are not.
It is common to perform validation on the frontend, but that should never be the only layer of validation. APIs can be called by mobile applications, other services, Postman, or completely different clients.
The backend should always treat incoming data as untrusted.
Spring Boot makes basic validation simple with Bean Validation:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public class ProductRequest { @NotBlank private String name; @Positive private BigDecimal price; @PositiveOrZero private int stockQuantity; } |
Using @Valid in the controller allows invalid requests to be rejected before they reach the business logic.
The idea is simple:
Validate data as early as possible.
This prevents invalid information from reaching the service or database layer unnecessarily.
- HTTP Status Codes Are Part of the API Contract
An API should clearly communicate the result of a request.
Consider:
GET /api/products/101
If product 101 exists, returning 200 OK makes sense.
But what if it doesn’t?
Returning:
200 OK
null
forces the client to guess what happened.
A better response would be:
404 Not Found
with a meaningful error body:
|
1 2 3 4 5 6 7 |
{ "code": "PRODUCT_NOT_FOUND", "message": "Product not found with id: 101" } |
HTTP already provides status codes that describe common outcomes.
For example:
- 200 OK — Request completed successfully.
- 201 Created — A new resource was created.
- 400 Bad Request — The request contains invalid data.
- 404 Not Found — The requested resource doesn’t exist.
- 409 Conflict — The request conflicts with the current state of the application.
- 500 Internal Server Error — An unexpected server-side failure occurred.
Using these codes correctly makes the API easier to understand.
One important point is to avoid using 500 as a generic error response. A validation failure is different from an unexpected database or application failure.
- Don’t Put All Error Handling Inside Controllers
As an application grows, controllers can quickly become filled with error-handling logic.
For example:
|
1 2 3 4 5 6 7 8 9 |
try { return productService.getProduct(id); } catch (ProductNotFoundException e) { // return 404 } |
Doing this separately for products, customers, orders, and other resources leads to duplicated code.
It can also result in different controllers handling similar errors in different ways.
Spring provides @RestControllerAdvice to centralize exception handling.
The flow becomes:
Request
↓
Controller
↓
Service
↓
Exception
↓
Global Exception Handler
↓
HTTP Response
For example:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
@RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(ProductNotFoundException.class) public ResponseEntity<ApiError> handleProductNotFound( ProductNotFoundException ex) { return ResponseEntity .status(HttpStatus.NOT_FOUND) .body(new ApiError( "PRODUCT_NOT_FOUND", ex.getMessage() )); } } |
Now the controller can focus on handling the request, while the global exception handler is responsible for converting known exceptions into appropriate API responses.
This keeps error-handling logic centralized and easier to maintain.
- Keep Error Responses Consistent
Status codes tell us what happened, but the response body should also follow a predictable structure.
Imagine one endpoint returns:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
{ "error": "Product not found" } while another returns: { "message": "No product exists" } and another returns: { "status": 404, "errorMessage": "Invalid product ID" } |
All three may represent similar failures, but their structures differ.
This makes API consumption unnecessarily complicated.
A consistent structure is easier to work with:
{
“code”: “PRODUCT_NOT_FOUND”,
“message”: “Product not found with id: 101”
}
The exact structure depends on the application. There is no single format that every API must follow.
What matters is consistency.
If frontend applications, mobile clients, or other backend services consume the API, they should be able to understand errors without having to implement different logic for every endpoint.
- Keep Controllers Focused
A common mistake in backend applications is putting too much logic inside controllers.
A controller should primarily deal with HTTP concerns:
Request
↓
Controller
↓
Service
↓
Repository
↓
Database
The controller receives the request and passes the relevant information to the service.
The service handles business logic.
The repository handles data access.
Validation handles invalid input.
Exception handling deals with failures.
Keeping these responsibilities separate makes the code easier to understand and maintain.
For example, a controller shouldn’t need to know how a product is stored in the database or how a particular business rule is calculated.
Its main responsibility is to translate between the HTTP and application layers.
- API Design Also Affects Testing
A predictable API is much easier to test.
Consider a product API.
A successful creation should result in:
Valid request
↓
201 Created
An invalid request:
Invalid price
↓
400 Bad Request
A missing resource:
Product doesn’t exist
↓
404 Not Found
A duplicate resource:
Product already exists
↓
409 Conflict
An unexpected failure:
Server failure
↓
500 Internal Server Error
Tests can verify not only that the API returns a response, but that it returns the correct response for a particular situation.
This becomes increasingly important as an application grows and multiple developers or teams start consuming the same APIs.
- Common Mistakes to Avoid
A few mistakes appear frequently when designing REST APIs.
Returning 200 OK for every situation
A successful request and a failed request should not look identical.
Relying only on frontend validation
The backend must validate requests independently.
Returning raw exceptions
Internal exception messages can expose implementation details that clients don’t need to know.
Duplicating exception handling
If every controller handles the same exceptions differently, maintaining the API becomes harder.
Inconsistent error structures
Consumers should not need to learn a different error format for every endpoint.
Putting business logic in controllers
Controllers become difficult to maintain when they are responsible for validation, business rules, database operations, and error handling all at once.
Conclusion
A REST API is more than a collection of endpoints that return data.
A production API also needs to define what happens when requests are invalid, resources don’t exist, operations conflict, or unexpected failures occur.
Backend validation protects the application from invalid input.
HTTP status codes communicate the outcome of a request.
Centralized exception handling keeps error management maintainable.
Consistent error responses make APIs easier to consume and test.
These concepts are not particularly complicated. The important part is applying them consistently.
Good API design ultimately comes down to one simple principle:
Don’t make the consumer guess what happened.
Whether a request succeeds or fails, the API should communicate the result clearly, consistently, and predictably.
Drop a query if you have any questions regarding REST API, and we will get back to you quickly.
Empowering organizations to become ‘data driven’ enterprises with our Cloud experts.
- Reduced infrastructure costs
- Timely data-driven decisions
About CloudThat
FAQs
1. Why shouldn't every API response return 200 OK?
ANS: – Because different outcomes have different meanings. A successful request, invalid input, missing resource, and server failure should be distinguishable by the client.
2. Why validate data on the backend if the frontend already does it?
ANS: – Because the API can be called directly by different clients. The backend should never assume that incoming data has already been validated.
3. What is the advantage of @RestControllerAdvice?
ANS: – It centralizes exception handling so that controllers don’t need to duplicate the same error-handling logic.
WRITTEN BY Minhajul Islam
Minhajul works as a Junior Java Developer at CloudThat, specializing in backend development with Java and Spring Boot. He works on building scalable backend services, REST APIs, and microservices, with hands on experience in databases, AWS, Redis, and Docker. He focuses on writing clean, maintainable code and designing reliable backend systems while working with modern cloud native technologies.
Login

August 26, 2026
PREV
Comments