HTTP Status Codes & REST Semantics
HTTP Status Codes & REST Semantics
Returning the correct HTTP status code is not cosmetic — it is the primary signalling mechanism your API has with every client, proxy, cache, and monitoring tool in the chain. A well-chosen code tells the caller whether to retry, parse a body, redirect, log an alert, or do nothing. Getting codes wrong erodes trust in your API and forces clients to write defensive workarounds. This lesson maps the standard status families to real REST operations and shows how Spring Boot lets you control them precisely.
The Five Status Families
HTTP status codes fall into five families, each with a shared meaning:
- 1xx — Informational: the request is being processed. Rare in REST APIs; you will seldom send these yourself.
- 2xx — Success: the operation completed as requested. This is where you spend most of your design energy.
- 3xx — Redirection: the client must take another action, usually follow a new URL.
- 4xx — Client error: the request is bad. The client must fix it before retrying.
- 5xx — Server error: the server failed. The client may retry later.
The 2xx Family — Matching Code to Operation
Most developers default to 200 OK for everything. This works, but loses precision. The right code:
- 200 OK — a read or a full replacement succeeded and the response body carries the result. Use for
GET,PUT(when returning the updated resource), andPOSTactions that are not resource creation. - 201 Created — a new resource was created. Use for
POST(and occasionallyPUT). The response should include aLocationheader pointing at the new resource URL. - 204 No Content — the operation succeeded but there is nothing to return. Use for
DELETE, and forPUT/PATCHwhen you do not need to echo the updated representation back. - 202 Accepted — the request has been accepted for asynchronous processing but is not yet complete. The body usually contains a task or job ID the client can poll.
Returning 201 and Location in Spring Boot
Use ResponseEntity together with UriComponentsBuilder or ServletUriComponentsBuilder to build the Location URI cleanly:
ResponseEntity.created(uri) is a factory method that sets the status to 201 and the Location header in one call. Appending .body(saved) is optional — some teams omit it to keep the response lean.
Returning 204 for DELETE
Using Void as the generic type makes it explicit that no body is expected. ResponseEntity.noContent().build() is the idiomatic factory.
The 4xx Family — Client Errors
These codes tell the client it must fix the request. Do not collapse them all to 400:
- 400 Bad Request — the request body or parameters are syntactically or semantically invalid. Use when validation fails.
- 401 Unauthorized — the client is not authenticated. The name is misleading; it really means "unauthenticated." Spring Security sets this automatically for protected endpoints.
- 403 Forbidden — the client is authenticated but does not have permission for this operation.
- 404 Not Found — the requested resource does not exist. Never use 200 with a null body when a record is absent.
- 405 Method Not Allowed — the HTTP method is not supported for this path (Spring sets this automatically).
- 409 Conflict — the operation cannot complete due to a conflict with the current state of the resource, for example a duplicate email during registration.
- 422 Unprocessable Entity — the syntax is valid but the business rules reject the data. Many teams prefer this over 400 for validation errors to distinguish parse failures from logic failures.
- 429 Too Many Requests — the client has exceeded a rate limit.
Throwing Exceptions That Map to Status Codes
Cluttering every controller method with status-code decisions is noisy. The cleaner pattern is to throw a domain-specific exception and let a centralized @ControllerAdvice map it:
ProblemDetail (introduced in Spring 6 / Spring Boot 3) implements RFC 9457 (formerly RFC 7807), the standard problem-detail format. Clients receive a JSON body with type, title, status, and detail fields they can handle programmatically.
Using @ResponseStatus on Exception Classes
For simple cases you can annotate the exception itself instead of writing an @ExceptionHandler:
Spring will set the status code automatically when this exception propagates out of a controller. The trade-off: you cannot customise the response body this way, so prefer @ExceptionHandler + ProblemDetail for any production API where clients need structured error details.
The 5xx Family — When to Use 500 vs 503
Avoid writing code that deliberately throws 500. Let unhandled exceptions fall through to Spring's default handler, which returns 500. Use 503 Service Unavailable when a dependency (database, external service) is temporarily down and you want to signal that the client should retry — optionally include a Retry-After header. 502 Bad Gateway and 504 Gateway Timeout come from infrastructure layers (reverse proxies, load balancers), not your application code.
{"success": false, "error": "not found"} wrapped in a 200 response break every HTTP-aware tool in existence: caches store the error, monitoring graphs it as success, and retry logic skips it. Use the correct status code so the protocol layer works for you.
REST Semantics: Idempotency and Safety
Status codes alone do not tell the full story. Two HTTP properties shape which codes are appropriate:
- Safe methods (
GET,HEAD) must not change server state. They should never return201or204. - Idempotent methods (
GET,PUT,DELETE,HEAD,OPTIONS) produce the same result no matter how many times they are called. ADELETEon a resource that no longer exists should return404the first time and404again on a repeat — not a different code, not an error about "already deleted." POSTis neither safe nor idempotent. CallingPOST /productstwice should create two products, and the second call returns201again (or409if duplicates are forbidden).
Quick Reference Table
GET /resources— 200 OK (list), 404 impossible (list is empty = 200 +[])GET /resources/{id}— 200 OK or 404 Not FoundPOST /resources— 201 Created + Location, or 400/409 on errorPUT /resources/{id}— 200 OK (with body) or 204 No Content (no body); 404 if missingPATCH /resources/{id}— 200 OK or 204 No Content; 404 or 422 on errorDELETE /resources/{id}— 204 No Content; 404 if missing
Summary
Choosing the correct status code is a deliberate design act. Return 201 with a Location header when you create, 204 when you delete or update without echoing the body, 404 when a resource is absent, and the appropriate 4xx when the client sends bad data. Centralise status-code decisions in a @RestControllerAdvice using ProblemDetail, and never abuse 200 to hide failures. With these habits your API communicates precisely over HTTP the same language every proxy, client, and monitoring tool already speaks.