REST API Design Interview Questions
What is the difference between @Controller and @RestController?
@Controller returns view names (Thymeleaf/JSP) unless a method has @ResponseBody. @RestController = @Controller + @ResponseBody on every method: return values are written to the response body as JSON by Jackson's HttpMessageConverter.
@PathVariable vs @RequestParam vs @RequestBody?
| Annotation | Reads from | Example |
|---|---|---|
@PathVariable | URL path segment, identifies a resource | GET /api/products/42 |
@RequestParam | query string, filter/sort/page | GET /api/products?category=mobiles&page=0 |
@RequestBody | JSON body, mapped to an object | POST /api/products with {"name":"..."} |
How do you return proper HTTP status codes?
Return ResponseEntity (full control of status, headers, body) or put @ResponseStatus on the method or on an exception class.
@PostMapping
public ResponseEntity<ProductResponse> create(@Valid @RequestBody CreateProductRequest req) {
ProductResponse saved = productService.create(req);
URI location = URI.create("/api/products/" + saved.id());
return ResponseEntity.created(location).body(saved); // 201 + Location header
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT) // 204
public void delete(@PathVariable Long id) { productService.delete(id); }Which status codes should a REST API use?
| Code | When |
|---|---|
| 200 OK | successful GET / PUT / PATCH with body |
| 201 Created | POST created a resource (add Location) |
| 204 No Content | successful DELETE or update with no body |
| 400 Bad Request | validation failed, malformed JSON |
| 401 / 403 | not authenticated / authenticated but not allowed |
| 404 Not Found | resource does not exist |
| 409 Conflict | duplicate email, version conflict, insufficient stock |
| 500 | unexpected server error (never leak stack traces) |
PUT vs PATCH vs POST, and what is idempotency?
An operation is idempotent if repeating it gives the same result. GET, PUT and DELETE are idempotent; POST is not (two calls create two orders). PUT replaces the whole resource; PATCH updates only the fields sent; POST creates or triggers an action. For payment/order POSTs, accept an Idempotency-Key header so client retries do not charge twice (see the Coding Round).
How do you handle exceptions globally in Spring Boot?
Use @RestControllerAdvice with @ExceptionHandler methods. Spring Boot 3 supports RFC 7807 ProblemDetail for a standard error format.
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ProductNotFoundException.class)
public ProblemDetail handleNotFound(ProductNotFoundException ex) {
ProblemDetail pd = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
pd.setTitle("Product not found");
return pd;
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ProblemDetail handleValidation(MethodArgumentNotValidException ex) {
ProblemDetail pd = ProblemDetail.forStatus(HttpStatus.BAD_REQUEST);
pd.setTitle("Validation failed");
Map<String, String> errors = new LinkedHashMap<>();
ex.getBindingResult().getFieldErrors()
.forEach(e -> errors.put(e.getField(), e.getDefaultMessage()));
pd.setProperty("errors", errors);
return pd;
}
}Example
curl -i localhost:8080/api/products/999Example output
HTTP/1.1 404
Content-Type: application/problem+json
{"type":"about:blank","title":"Product not found","status":404,"detail":"Product 999 not found","instance":"/api/products/999"}How does request validation work?
Add spring-boot-starter-validation, put jakarta.validation.constraints annotations on the DTO and @Valid on the parameter. Invalid input throws MethodArgumentNotValidException (400). Use @Validated on the class to validate @PathVariable/@RequestParam too.
public record CreateProductRequest(
@NotBlank String name,
@Size(max = 500) String description,
@NotNull @Positive BigDecimal price,
@PositiveOrZero int stock,
@NotNull Long categoryId) { }Why use DTOs instead of returning entities?
- Entities can leak internal or sensitive fields (password hash, audit columns).
- Lazy JPA relations cause
LazyInitializationExceptionor infinite JSON recursion. - The API contract stays stable when the database schema changes.
- Request DTOs carry validation rules for that operation only (create vs update).
Java records make concise DTOs; map with a small method or MapStruct.
How do you implement pagination and sorting?
@GetMapping
public Page<ProductResponse> list(@RequestParam(required = false) String category,
@PageableDefault(size = 20, sort = "price") Pageable pageable) {
return productService.search(category, pageable);
}
// GET /api/products?category=mobiles&page=1&size=10&sort=price,descNever return an unbounded list from a large table. For infinite scroll on big tables prefer keyset (cursor) pagination: WHERE id > :lastId ORDER BY id LIMIT 20.
How do you version a REST API?
| Approach | Example |
|---|---|
| URI (most common in India projects) | /api/v1/products |
| Header | X-API-Version: 2 |
| Media type | Accept: application/vnd.shopease.v2+json |
| Query param | ?version=2 |
Keep old versions working until clients migrate; add fields in a backward-compatible way instead of creating a new version for every change.
How do you enable CORS for a React frontend?
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("http://localhost:5173", "https://shopease.in")
.allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE")
.allowCredentials(true);
}
}For one controller, @CrossOrigin(origins = "...") is enough. With Spring Security, also call http.cors(Customizer.withDefaults()).