skip to content

Walk through the ResponseEntity builder/factory API: what do ok(), created(), notFound(), and noContent() do, and how do you attach custom headers?

level: middleimportance: must knowfreq 72%

answer

  1. created(uri) auto-sets Location
  2. notFound/noContent = HeadersBuilder, no .body()
  3. status(HttpStatus) or status(int) escape hatch
  4. .header() repeatable, .headers(consumer)
  5. terminal: .body() vs .build()

basics

~10 s

ok() = 200, created(uri) = 201 with a Location header, notFound() = 404, noContent() = 204. Each returns a builder; you add headers via .header(name, value) or .headers(HttpHeaders), then finish with .body(x) or .build().

solid answer

~30 s

ResponseEntity exposes static factories returning a BodyBuilder or HeadersBuilder. ResponseEntity.ok() / ok(body) → 200. ResponseEntity.created(URI) → 201 and sets the Location header to that URI. ResponseEntity.notFound() → 404 (HeadersBuilder, no body). ResponseEntity.noContent() → 204. ResponseEntity.accepted() → 202. ResponseEntity.badRequest() → 400. For an arbitrary status use ResponseEntity.status(HttpStatus.X) or status(int). On the builder you attach headers with .header(name, values...) (repeatable), .headers(HttpHeaders), or typed shortcuts like .contentType(MediaType.APPLICATION_JSON), .eTag(...), .cacheControl(...), .lastModified(...). You terminate with .body(payload) for a body or .build() for none. HeadersBuilder factories (notFound, noContent) only expose .build() since those responses carry no body.

code

java · 16 lines
java
@PostMapping("/orders")
public ResponseEntity<OrderDto> create(@RequestBody @Valid CreateOrder cmd,
                                      UriComponentsBuilder ucb) {
    OrderDto saved = orderService.create(cmd);
    URI location = ucb.path("/orders/{id}").buildAndExpand(saved.id()).toUri();
    return ResponseEntity.created(location)          // 201 + Location header
            .header("X-Order-Version", "1")
            .cacheControl(CacheControl.noStore())
            .body(saved);
}

@DeleteMapping("/orders/{id}")
public ResponseEntity<Void> delete(@PathVariable Long id) {
    orderService.delete(id);
    return ResponseEntity.noContent().build();       // 204, no body
}

go deeper

for a junior

Recognize ok/created/notFound/noContent and their status codes.

for a middle

Explain BodyBuilder vs HeadersBuilder and that created() sets Location; know header-attachment methods.

for a senior

Discuss typed header shortcuts (eTag, cacheControl) and status(int) for non-enum codes.

for a principal

Consider building Location URIs safely via UriComponentsBuilder/ServletUriComponentsBuilder and consistent header policy.

## The two builder interfaces `ResponseEntity` static methods return one of two nested builder types: - **`ResponseEntity.BodyBuilder`** — can set headers *and* a body. Terminal methods: `.body(T)` or `.build()` (which yields `ResponseEntity<Void>`). - **`ResponseEntity.HeadersBuilder<?>`** — headers only, no body. Terminal method: `.build()`. ## The status factories | Factory | Status | Returns | Notes | |---|---|---|---| | `ok()` / `ok(body)` | 200 OK | BodyBuilder / ResponseEntity | most common success | | `created(URI location)` | 201 Created | BodyBuilder | **sets the `Location` header** to the URI | | `accepted()` | 202 Accepted | BodyBuilder | async accepted | | `noContent()` | 204 No Content | HeadersBuilder | no body by definition | | `badRequest()` | 400 | BodyBuilder | | | `notFound()` | 404 | HeadersBuilder | no body | | `unprocessableEntity()` | 422 | BodyBuilder | | | `status(HttpStatus)` / `status(int)` | any | BodyBuilder | escape hatch for any code | `ok(body)` and `created(uri).body(dto)` are the convenience forms; everything reduces to `status(...).<headers>.body(...)`. ## Attaching headers On any builder: - `.header(String name, String... values)` — add a header, repeatable; multiple values or multiple calls accumulate. - `.headers(HttpHeaders headers)` — copy in a prepared `HttpHeaders` object. - `.headers(Consumer<HttpHeaders>)` — mutate headers with a lambda. - Typed shortcuts: `.contentType(MediaType)`, `.contentLength(long)`, `.eTag(String)`, `.lastModified(...)`, `.cacheControl(CacheControl)`, `.location(URI)`, `.allow(HttpMethod...)`, `.varyBy(String...)`. ## Terminal methods - `.body(payload)` → `ResponseEntity<T>` with a body. - `.build()` → `ResponseEntity<Void>` with no body. Calling `.body(null)` is legal and produces an empty body but still lets you keep the generic type. ## Gotchas - **`created(uri)` is the only factory that auto-populates a header** (`Location`). The others set only status. - `notFound()` and `noContent()` return a `HeadersBuilder`, so there is **no `.body(...)`** — the compiler stops you from putting a body on a 204/404 via those factories. If you *need* a body with 404, use `status(HttpStatus.NOT_FOUND).body(errorDto)`. - `status(int)` accepts non-standard/custom codes; `status(HttpStatus)` is type-safe for standard ones. `HttpStatus.valueOf(int)` throws on unknown codes, whereas `status(int)` does not require a known enum. - Header names are case-insensitive per HTTP; `HttpHeaders` normalizes them. - The builder is **immutable-ish per call** in usage but you should not reuse a half-built builder across responses.

  • Why can't you call .body(...) after ResponseEntity.notFound()?
    notFound() returns a HeadersBuilder (headers only), which exposes .build() but not .body(). If you need a body with a 404, use ResponseEntity.status(HttpStatus.NOT_FOUND).body(dto) instead.
  • What's the difference between status(HttpStatus) and status(int)?
    status(HttpStatus) is type-safe for the standard enum values. status(int) accepts any integer, including non-standard/custom codes not present in the HttpStatus enum.

saying these in an interview costs you the question

  • Thinking notFound() lets you attach a body directly
  • Believing every factory sets a matching header (only created() sets Location)
  • Confusing .header() (add) with .headers() and thinking one replaces all headers

context