Home / Spring Boot / Web and REST / REST Controllers

REST Controllers

A REST controller receives HTTP requests, calls your service layer and returns data (usually JSON). Spring Boot converts Java objects to JSON and back automatically.

Mapping annotations

AnnotationHTTP methodTypical use
@GetMappingGETRead data
@PostMappingPOSTCreate data
@PutMappingPUTReplace data
@PatchMappingPATCHChange part of data
@DeleteMappingDELETEDelete data
@RequestMappingAnyClass-level base path

Complete controller

import java.net.URI;
import java.util.*;
import java.util.concurrent.atomic.AtomicLong;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;

public record Product(Long id, String name, double price) { }

@RestController
@RequestMapping("/api/products")
public class ProductController {

    private final Map<Long, Product> store = new LinkedHashMap<>();
    private final AtomicLong ids = new AtomicLong();

    @GetMapping                                            // GET /api/products?minPrice=10
    public List<Product> list(@RequestParam(defaultValue = "0") double minPrice) {
        return store.values().stream().filter(p -> p.price() >= minPrice).toList();
    }

    @GetMapping("/{id}")                                   // GET /api/products/5
    public ResponseEntity<Product> get(@PathVariable Long id) {
        Product p = store.get(id);
        return p == null ? ResponseEntity.notFound().build() : ResponseEntity.ok(p);
    }

    @PostMapping                                           // POST /api/products  (JSON body)
    public ResponseEntity<Product> create(@RequestBody Product input) {
        Product saved = new Product(ids.incrementAndGet(), input.name(), input.price());
        store.put(saved.id(), saved);
        URI location = ServletUriComponentsBuilder.fromCurrentRequest()
                .path("/{id}").buildAndExpand(saved.id()).toUri();
        return ResponseEntity.created(location).body(saved);   // 201 + Location header
    }

    @PutMapping("/{id}")
    public ResponseEntity<Product> update(@PathVariable Long id, @RequestBody Product input) {
        if (!store.containsKey(id)) return ResponseEntity.notFound().build();
        Product updated = new Product(id, input.name(), input.price());
        store.put(id, updated);
        return ResponseEntity.ok(updated);
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> delete(@PathVariable Long id) {
        return store.remove(id) == null ? ResponseEntity.notFound().build() : ResponseEntity.noContent().build();
    }
}
curl -X POST localhost:8080/api/products -H "Content-Type: application/json" -d '{"name":"Pen","price":10}'
curl localhost:8080/api/products
curl -X DELETE localhost:8080/api/products/1

Important HTTP status codes

CodeMeaningUse
200 OKSuccessGET, PUT
201 CreatedResource createdPOST (send a Location header)
204 No ContentSuccess, no bodyDELETE
400 Bad RequestInvalid inputValidation errors
401 UnauthorizedNot logged inMissing or bad token
403 ForbiddenLogged in but not allowedMissing role
404 Not FoundResource missingUnknown id
409 ConflictState conflictDuplicate value
500 Internal Server ErrorServer bugUnhandled exception

Other request features

How Spring binds arguments and writes the result

Parameter you writeResolver reads fromNotes
@PathVariable Long idURL template /products/{id}Converted by ConversionService, wrong type gives 400
@RequestParam String qQuery string ?q=required=true by default, defaultValue possible
@RequestBody ProductRequest rBody, via HttpMessageConverter (Jackson)@Valid triggers validation. Bad JSON gives 400
@RequestHeader / @CookieValueHeaders and cookies
@ModelAttribute (form data)Form fields or query parameters into an objectFor form posts, not JSON
HttpServletRequest / Principal / AuthenticationServlet API and securityInjected by argument resolvers
Pageable / Sort?page=&size=&sort=Needs Spring Data web support (auto-configured)
MultipartFilemultipart/form-dataSee the file upload page
Return typeWhat happens
POJO / record / ListWritten as JSON with status 200
ResponseEntity<T>Full control of status, headers and body
void + @ResponseStatusEmpty body with the status
String in @ControllerA view name (Thymeleaf template). In @RestController: plain text
Callable / DeferredResult / CompletableFutureAsync: the request thread is released and the result is written later
StreamingResponseBody / SseEmitterStreaming large or long-lived responses
ProblemDetailRFC 9457 error body

Content negotiation

@GetMapping(value = "/api/products/{id}", produces = { MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE })
public Product get(@PathVariable Long id) { return service.get(id); }
// Accept: application/json  -> JSON       Accept: application/xml -> XML (needs jackson-dataformat-xml)
// No matching type -> 406 Not Acceptable      Body in a wrong Content-Type -> 415 Unsupported Media Type

Conditional GET with ETag (saves bandwidth)

@GetMapping("/api/products/{id}")
public ResponseEntity<ProductResponse> get(@PathVariable Long id, WebRequest request) {
    ProductResponse product = service.get(id);
    String etag = "\"" + product.version() + "\"";
    if (request.checkNotModified(etag)) {            // compares If-None-Match with the ETag
        return null;                                 // Spring sends 304 Not Modified with an empty body
    }
    return ResponseEntity.ok().eTag(etag).cacheControl(CacheControl.maxAge(Duration.ofMinutes(1))).body(product);
}

Async return types

@GetMapping("/api/reports/{id}")
public CompletableFuture<Report> report(@PathVariable Long id) {
    return CompletableFuture.supplyAsync(() -> reportService.build(id), executor);     // servlet thread is free while it runs
}

@GetMapping("/api/slow")
public Callable<String> slow() {
    return () -> { Thread.sleep(2000); return "done"; };          // runs on the MVC async executor
}
// spring.mvc.async.request-timeout=30s

HTTP semantics to respect

MethodSafeIdempotentBodyTypical status
GETYesYesNo200, 304, 404
POSTNoNoYes201 with Location, 202 for async work
PUTNoYesYes (full)200 or 204
PATCHNoNot guaranteedYes (partial)200 or 204
DELETENoYesUsually no204, 404 is also acceptable

API design rules that age well

Common mistake: Returning entities directly from controllers. It exposes your database structure and can trigger lazy loading errors. Return DTOs (records) instead.

Interview questions

What is the difference between @Controller and @RestController?

@RestController adds @ResponseBody to every method, so return values are written to the response body (JSON) instead of being treated as view names.

What is the difference between PUT and PATCH?

PUT replaces the whole resource. PATCH changes only some fields.

@RequestParam or @PathVariable?

@PathVariable reads a part of the URL path (/products/5). @RequestParam reads a query parameter (?page=2).

How do I return 201 with a Location header?

Use ResponseEntity.created(uri).body(dto). Build the uri from the request with ServletUriComponentsBuilder.

Why does a request with a wrong JSON field type return 400?

Jackson throws HttpMessageNotReadableException while reading the body. Spring maps it to 400. You can customize the response in @RestControllerAdvice.