REST questions split cleanly in two: what REST is as an architectural style, and how Spring implements it. Both get asked, and people usually prepare only the second.
The style
What does REST stand for?
REpresentational State Transfer. The client transfers representations of resource state — JSON, XML, an image — over a uniform interface.
What is a resource?
Anything worth naming with a URI: a product, an order, a collection of orders. The URI identifies
the resource; the representation is what you get back. /api/products/{id} is the
resource, the JSON body is one representation of it.
What does CRUD mean, and which verbs map to it?
| Operation | Verb | Typical URI |
|---|---|---|
| Create | POST | /api/products |
| Read | GET | /api/products, /api/products/{id} |
| Update | PUT (whole) / PATCH (partial) | /api/products/{id} |
| Delete | DELETE | /api/products/{id} |
Is REST stateless?
Yes — that is one of its constraints. Every request carries everything needed to serve it; the
server keeps no client session between requests. The pizza API is
SessionCreationPolicy.STATELESS and carries identity in a JWT for exactly this reason:
any instance can serve any request, so scaling out is just adding instances.
Is REST scalable and interoperable?
Scalable because it is stateless and cacheable — no server affinity, and GETs can be served from a cache or a CDN. Interoperable because it is plain HTTP and a media type, so any client on any platform can call it.
Safe and idempotent
What are safe operations? Ones that do not change server state. GET, HEAD, OPTIONS. A GET that changes something is a bug — crawlers, prefetchers and browsers all assume otherwise.
What are idempotent operations, and why does it matter? Ones where doing it n times has the same effect as doing it once.
| Verb | Safe | Idempotent |
|---|---|---|
| GET | yes | yes |
| HEAD | yes | yes |
| PUT | no | yes |
| DELETE | no | yes |
| POST | no | no |
| PATCH | no | not necessarily |
It matters because networks fail. A client that sends a request and never sees the response cannot tell whether it was processed, so it retries — and if the operation is idempotent, retrying is safe. POST is not idempotent, which is why the payment call in the pizza API carries an idempotency key:
// Without a key, a request that succeeded at Stripe but whose response was lost to a
// network blip gets retried and creates a SECOND PaymentIntent - the classic double
// charge. Keyed on our own order id, Stripe recognises the replay and returns the
// original instead.
RequestOptions options =
RequestOptions.builder().setIdempotencyKey("order-" + orderId).build();
return stripe.paymentIntents().create(params.build(), options);Status codes
What should each operation return on success?
| Operation | Success |
|---|---|
| GET | 200 OK |
| POST creating something | 201 Created, with a Location header |
| PUT / PATCH | 200 OK, or 204 if no body is returned |
| DELETE | 204 No Content |
| Accepted for later processing | 202 Accepted |
And the failures worth distinguishing:
| Code | Means |
|---|---|
| 400 Bad Request | the request is malformed or fails validation |
| 401 Unauthorized | not authenticated — despite the name |
| 403 Forbidden | authenticated, but not allowed |
| 404 Not Found | no such resource |
| 409 Conflict | violates current state — duplicate, version clash |
| 422 Unprocessable | syntactically fine, semantically wrong |
| 500 | our fault. Never a client's bad input. |
The 401/403 distinction and the "400 not 500 for bad input" rule are the two that get asked.
Spring's annotations
Is @Controller a stereotype? Is @RestController?
Both are — each is meta-annotated with @Component, so both are found by component
scanning.
What is the difference between them?
@RestController is @Controller + @ResponseBody at class
level. So every method's return value is serialised into the response body instead of being treated
as a view name.
When do you need @ResponseBody? On a method in a plain
@Controller that should return data rather than a view. Never inside a
@RestController — it is already implied.
@RequestBody versus @ResponseBody — do not muddle
them.
@RequestBodyis on a parameter: deserialise the incoming body into this object.@ResponseBodyis on a method or class: serialise the return value into the outgoing body.
@PostMapping
public ResponseEntity<ProductDTO> createProduct(@Valid @RequestBody ProductCreateDTO dto) {
return new ResponseEntity<>(productService.createProduct(dto), CREATED);
}When do you need @ResponseStatus? To set a status other than 200
without building a ResponseEntity — on a handler method, or on an exception class so
that throwing it produces that status. ResponseEntity is the more flexible option
because it can also set headers, and it lets one method return different statuses.
Which starter would you use for a Spring REST application?
spring-boot-starter-webmvc, which brings Spring MVC, Jackson and Tomcat. It was
called spring-boot-starter-web before Boot 4; that name still resolves but is
deprecated.
HttpMessageConverter
What is an HttpMessageConverter?
The thing that turns a Java object into an HTTP body and back. Each one declares which media types
and which Java types it supports. MappingJackson2HttpMessageConverter handles
application/json; there are converters for strings, byte arrays, form data and
resources.
How is one chosen? By content negotiation. For the request, from the
Content-Type header; for the response, from the Accept header, narrowed by
the produces attribute of the mapping. If nothing matches you get 415 Unsupported Media
Type or 406 Not Acceptable — which is what those two codes actually mean.
Return Resource, not byte[], for a file. Spring streams
a Resource, so a 2 MB image never sits in heap in its entirety; with
byte[] the whole file is buffered per concurrent request.
@GetMapping("/images/{fileName}")
public ResponseEntity<Resource> getImage(@PathVariable String fileName) {
Resource image = imageStorage.load(fileName);
return ResponseEntity.ok()
.contentType(MediaType.IMAGE_JPEG)
// The name contains a UUID, so the bytes behind a URL never change.
.cacheControl(CacheControl.maxAge(Duration.ofDays(365)).cachePublic())
// "inline", not "attachment" - this renders on a page, it is not a download.
.header("Content-Disposition", "inline; filename=\"" + fileName + "\"")
.body(image);
}Validation and errors
How do you validate a request body?
@Valid on the parameter and constraints on the DTO. A failure raises
MethodArgumentNotValidException before the method body runs.
How do you return a consistent error? One
@RestControllerAdvice, so the logic lives in one place instead of as a try/catch in
every handler.
@RestControllerAdvice
public class RestExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiError> handleValidation(
MethodArgumentNotValidException ex, HttpServletRequest request) {
ApiError error = new ApiError(BAD_REQUEST, "Validation failed", request.getRequestURI());
for (FieldError fieldError : ex.getBindingResult().getFieldErrors()) {
error.addValidationError(fieldError.getField(), fieldError.getDefaultMessage());
}
return ResponseEntity.badRequest().body(error);
}
// Without this, the catch-all below turns a malformed UUID into a 500 - which is
// wrong and actively misleading. The server is fine; the request is not.
@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseEntity<ApiError> handleUnreadableBody(
HttpMessageNotReadableException ex, HttpServletRequest request) { ... }
@ExceptionHandler(AuthenticationException.class)
public ResponseEntity<ApiError> handleAuthentication(
AuthenticationException ex, HttpServletRequest request) {
// Deliberately vague: "no such user" vs "wrong password" tells an attacker
// which email addresses are registered.
ApiError error = new ApiError(UNAUTHORIZED, "Invalid email or password",
request.getRequestURI());
return ResponseEntity.status(error.getStatus()).body(error);
}
/** Catch-all. Logs the real cause but never leaks a stack trace to the client. */
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiError> handleUnexpected(Exception ex, HttpServletRequest request) {
log.error("Unhandled exception on {}", request.getRequestURI(), ex);
ApiError error = new ApiError(INTERNAL_SERVER_ERROR, "Something went wrong on our end",
request.getRequestURI());
return ResponseEntity.status(error.getStatus()).body(error);
}
}One predictable error envelope means the client has exactly one error path to handle instead of guessing at whatever each framework default happens to produce.
⚠️ Never expose entities
Return DTOs. An entity as a response body leaks your schema, drags lazy associations into
serialisation (and often into a LazyInitializationException), and makes every column
rename a breaking API change. As a request body it is worse: a client could set any field
that exists, including role or price. In the pizza API every price is
computed on the server and the client never sends one — that is a security boundary, not a style
preference.
Calling a REST API from Spring
What are the advantages of RestTemplate?
It handles connection management, message conversion and error translation, so calling an API is
one line instead of a try/finally around a HttpURLConnection.
⚠️ But it is no longer the answer to "which client would you use".
RestTemplate is still supported and still works, but Spring 6.1 introduced
RestClient: the same synchronous behaviour with a fluent API. Every study guide written
before 2024 names RestTemplate; the current answer is:
| Client | Use for |
|---|---|
RestClient | synchronous calls — the default choice today |
WebClient | reactive or streaming calls |
@HttpExchange interface | a declarative client, like a Feign client but built in |
RestTemplate | existing code |
// RestClient - the modern synchronous client
RestClient client = RestClient.create("http://localhost:8085");
List<ProductDTO> menu = client.get()
.uri("/api/products")
.retrieve()
.body(new ParameterizedTypeReference<>() {});
ProductDTO created = client.post()
.uri("/api/admin/products")
.header("Authorization", "Bearer " + token)
.contentType(MediaType.APPLICATION_JSON)
.body(dto)
.retrieve()
.body(ProductDTO.class);The ParameterizedTypeReference is there because of type erasure: without it there is
no way to tell the converter you want a List<ProductDTO> rather than a list of
maps.
Securing a REST API
Is REST secure? What can you do about it?
REST says nothing about security — it is an architectural style, not a protocol. In practice: TLS everywhere; a bearer token or OAuth2 rather than a session cookie; authorisation checked on every request; validate all input; expose UUIDs rather than sequential ids so nobody can enumerate your resources; and rate limiting. See Security for how Spring does it.
What to remember
- Safe = no state change. Idempotent = repeating it is harmless. POST is neither, which is why retries need an idempotency key.
- 201 for a create, 204 for a delete, 401 for "not signed in", 403 for "not allowed", and never 500 for bad input.
@RequestBodyon a parameter,@ResponseBodyon a method.@RestControllerimplies the latter.- Message converters plus
Accept/Content-Typedecide the format; 406 and 415 are what "no converter matched" looks like. - DTOs in, DTOs out. Entities never cross the wire.
RestClient, notRestTemplate, for new code.