Request bodies and status codes

Spring Boot · Essentials · 12 min

Creating and updating resources is half of every REST API.

For POST, PUT and PATCH, the client sends JSON in the body. Annotate a parameter with @RequestBody and Jackson deserialises it into your type. Records work well as request DTOs: immutable, concise, and the JSON field names match the component names. Unknown JSON properties are ignored by default in Spring Boot; malformed JSON gives 400.

Use separate DTOs for input and output rather than exposing your JPA entities directly. That keeps clients from setting fields like id or createdAt, and lets the API evolve independently of the database.

Status codes matter:

  • 201 Created with a Location header after creating something: ResponseEntity.created(uri).body(dto). ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}") builds the URI.
  • 204 No Content after a successful delete or an update that returns nothing: ResponseEntity.noContent().build(), or annotate the method with @ResponseStatus(HttpStatus.NO_CONTENT).
  • 404 when the target doesn't exist, 409 for conflicts.

PUT replaces a resource and should be idempotent; PATCH changes part of it. Keep them semantically correct and clients (and caches and retries) behave predictably.

Test quickly with curl -X POST -H 'Content-Type: application/json' -d '{...}' or an HTTP client in your IDE.

Example

record CreateTodo(String title) {}
record Todo(long id, String title, boolean done) {}
@RestController
@RequestMapping("/api/todos")
class TodoController {
    private final Map<Long, Todo> store = new ConcurrentHashMap<>();
    private final AtomicLong ids = new AtomicLong();
    @PostMapping
    ResponseEntity<Todo> create(@RequestBody CreateTodo req) {
        Todo t = new Todo(ids.incrementAndGet(), req.title(), false);
        store.put(t.id(), t);
        URI location = ServletUriComponentsBuilder.fromCurrentRequest()
            .path("/{id}").buildAndExpand(t.id()).toUri();
        return ResponseEntity.created(location).body(t);       // 201 + Location
    }
    @DeleteMapping("/{id}")
    ResponseEntity<Void> delete(@PathVariable long id) {
        return store.remove(id) != null
            ? ResponseEntity.noContent().build()                // 204
            : ResponseEntity.notFound().build();                // 404
    }
}
// curl -i -X POST localhost:8080/api/todos -H 'Content-Type: application/json' \
//      -d '{"title":"write lesson"}'  -> 201, Location: .../api/todos/1

Remember

  • @RequestBody deserialises JSON into a DTO (records work well)
  • Use separate request/response DTOs instead of entities
  • Return 201 + Location on create, 204 on delete, 404 when missing

Watch out for

  • Binding request bodies straight to JPA entities, letting clients overwrite ids or internal fields.

Check yourself

  1. Which response best fits a successful POST that created a resource?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.spring.io/spring-framework/reference/web/webmvc/mvc-controller/ann-methods/requestbody.html