Spring Study Guide – REST

August 14, 20267 min readUpdated 8/18/2026

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?

OperationVerbTypical URI
CreatePOST/api/products
ReadGET/api/products, /api/products/{id}
UpdatePUT (whole) / PATCH (partial)/api/products/{id}
DeleteDELETE/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.

VerbSafeIdempotent
GETyesyes
HEADyesyes
PUTnoyes
DELETEnoyes
POSTnono
PATCHnonot 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?

OperationSuccess
GET200 OK
POST creating something201 Created, with a Location header
PUT / PATCH200 OK, or 204 if no body is returned
DELETE204 No Content
Accepted for later processing202 Accepted

And the failures worth distinguishing:

CodeMeans
400 Bad Requestthe request is malformed or fails validation
401 Unauthorizednot authenticated — despite the name
403 Forbiddenauthenticated, but not allowed
404 Not Foundno such resource
409 Conflictviolates current state — duplicate, version clash
422 Unprocessablesyntactically fine, semantically wrong
500our 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.

  • @RequestBody is on a parameter: deserialise the incoming body into this object.
  • @ResponseBody is 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:

ClientUse for
RestClientsynchronous calls — the default choice today
WebClientreactive or streaming calls
@HttpExchange interfacea declarative client, like a Feign client but built in
RestTemplateexisting 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.
  • @RequestBody on a parameter, @ResponseBody on a method. @RestController implies the latter.
  • Message converters plus Accept/Content-Type decide the format; 406 and 415 are what "no converter matched" looks like.
  • DTOs in, DTOs out. Entities never cross the wire.
  • RestClient, not RestTemplate, for new code.