Foundation · Prepare

HTTP & REST Fundamentals

Read methods, status codes, headers, and resources fluently—the protocol contract every Spring controller expresses.

Primary source: RFC 9110: HTTP Semantics - Methods & Status Codes

65 min

Lesson 6 of 7

1d streak

The Architectural Question

Controllers that return 200 for every outcome or hide deletes behind POST are HTTP bugs before they are Spring bugs.

What this guide proves

  • HTTP methods and status codes
  • Resources and representations
  • Statelessness

HTTP is a message contract

HTTP defines the semantics of requests and responses exchanged between participants. HTTP/1.1 often makes those messages look textual when inspected directly, while HTTP/2 and HTTP/3 use different framing on the wire. The durable model is therefore not “a text protocol” but a message containing a method, target, fields, optional content, and a corresponding response containing a status, fields, and optional content.

create-task.http
POST /tasks HTTP/1.1
Host: localhost:8080
Content-Type: application/json
Accept: application/json

{"title":"Read Module 0"}

HTTP/1.1 201 Created
Location: /tasks/42
Content-Type: application/json

{"id":42,"title":"Read Module 0","status":"OPEN"}

The request target identifies where the operation is directed. Content-Type describes the representation being sent; Accept states which response media types the client can process. The 201 status communicates successful creation, and Location tells the client the URI of the newly created resource. JSON carries the representation, but it does not replace the method, status, or headers.

Active Recall Checkpoint

A response contains a valid JSON task but no status or content type. Is the API contract complete?

Show answer & explanation

No. Clients need the HTTP outcome and representation metadata as well as the JSON fields. The body is only one part of the message.

Model resources separately from representations

A resource is the concept addressed by a URI; a representation is one transferable description of its current state. /tasks/42 identifies a task resource, while a JSON object is one representation of it. This separation allows the server to evolve internal classes and database tables without exposing them as the public contract. Query parameters commonly filter, sort, or page a collection rather than inventing verb-shaped paths.

read-tasks.http
GET /tasks?status=OPEN&limit=20 HTTP/1.1
Accept: application/json

GET /tasks/42 HTTP/1.1
Accept: application/json

Keep identifiers and actions deliberate. POST /tasks/42/complete can be a defensible command-style endpoint when completion is a domain action with its own rules. PATCH /tasks/42 with {"status":"COMPLETED"} can be appropriate when partial state updates are part of the public model. REST does not require pretending every domain operation is CRUD; it asks you to preserve uniform HTTP semantics and resource-oriented contracts.

Your Java type is not automatically your public API

Use an explicit request or response record such as CreateTaskRequest or TaskResponse. Persistence entities and internal fields should not leak merely because a serializer can see them.

Choose methods by safety and idempotency

A safe method is intended for retrieval rather than a requested state change. An idempotent method has the same intended effect when an identical request is repeated once or several times. These properties matter to browsers, proxies, caches, retrying clients, and operators. They describe method semantics, not a promise that no logging, billing meter, or timestamp changes internally.

MethodTypical meaningSafeIdempotent
GETRetrieve a representationYesYes
POSTCreate or process according to resource semanticsNoNot guaranteed
PUTCreate or completely replace target stateNoYes
PATCHApply a partial modificationNoDepends on patch contract
DELETERemove the target associationNoYes

Content in a GET request has no generally defined semantics and can be rejected by implementations, so put retrieval inputs in the path, query, or headers unless a carefully controlled API defines otherwise. PUT should represent a complete desired state for its target. PATCH is for partial change, but a particular patch operation may or may not be idempotent.

Active Recall Checkpoint

A client sends the same DELETE twice. Must both responses have the same status?

Show answer & explanation

No. Idempotency concerns the intended server effect. The first response may be 204 and the second 404 while the resource remains deleted after either sequence.

Status codes make outcomes machine-readable

Choose the most specific stable outcome the client can act on. Success does not always mean 200: creation commonly returns 201, a successful operation with no response content can return 204, and an asynchronous operation may return 202. Client errors describe a request the server will not fulfill as sent; server errors describe failure while attempting an otherwise valid request.

OutcomeStatusTask API example
Successful retrieval or update200 OKReturn the current task representation
Resource created201 CreatedReturn task plus Location
Success with no content204 No ContentDelete without a response body
Malformed syntax or basic input400 Bad RequestInvalid JSON or blank title
Authentication required401 UnauthorizedNo valid credentials
Authenticated but forbidden403 ForbiddenTask belongs to another account
Resource absent404 Not FoundNo task with that id
Current state conflicts409 ConflictTransition is incompatible with current state
Precondition failed412 Precondition FailedIf-Match version is stale
Unexpected server failure500 Internal Server ErrorUnhandled defect or dependency failure
problem-detail.json
{
  "type": "https://example.com/problems/task-not-found",
  "title": "Task not found",
  "status": 404,
  "detail": "No task exists with id 99",
  "instance": "/tasks/99"
}

Break it deliberately: The 200 OK error trap & unsafe GET

Returning HTTP 200 with {"success": false, "error": "Database timed out"} prevents API gateways, load balancers, and Resilience4j circuit breakers from detecting failure rates. Similarly, triggering state mutations inside GET requests allows web crawlers and browser pre-fetchers to accidentally wipe out database records.

Active Recall Checkpoint

What standard should modern Spring Boot 3 APIs use for formatted error payloads?

Show answer & explanation

RFC 9457 Problem Details, represented in Spring 6 and Boot 3 by ProblemDetail, which combines proper 4xx/5xx HTTP status codes with structured type, title, detail, and instance fields.

Design JSON and concurrency together

JSON has objects, arrays, strings, numbers, booleans, and null. Java records make concise transport shapes because their components are explicit and immutable. Validate the request contract at the boundary, then map it into domain behavior. Avoid Java binary serialization, leaking stack traces, or exposing database-only columns as accidental API fields.

TaskContracts.java
record CreateTaskRequest(String title) {}

record TaskResponse(long id, String title, String status, long version) {}

record CompleteTaskRequest(long expectedVersion) {}

Concurrent clients may both read version 3 and try to change it. An entity tag or explicit expected version lets the server reject stale intent instead of silently overwriting it. For example, the client can send If-Match: "task-42-v3" and receive 412 if the current representation has changed. HTTP preconditions and database optimistic locking solve related parts of the same lost-update problem.

complete-task.http
PATCH /tasks/42 HTTP/1.1
Content-Type: application/json
If-Match: "task-42-v3"

{"status":"COMPLETED"}

Active Recall Checkpoint

Why not serialize the JPA entity directly?

Show answer & explanation

The persistence model has a different responsibility and lifecycle. An explicit response contract prevents lazy-loading details, internal columns, and schema changes from silently becoming public API.

Exercise a complete Task API contract

Run the supplied local fixture before sending requests. The lab must provide the server or mock implementation; it must not assume that a Spring application from a later module already exists. Create a task, follow its Location, list tasks, complete it with the expected version, and delete it. For each exchange, record method, target, status, important headers, and body.

TaskApi.http
### Create
POST http://localhost:8080/tasks
Content-Type: application/json

{"title":"Read Module 0"}

### Read
GET http://localhost:8080/tasks/42
Accept: application/json

### Complete
PATCH http://localhost:8080/tasks/42
Content-Type: application/json
If-Match: "task-42-v1"

{"status":"COMPLETED"}

Then build a failure matrix: blank title, malformed JSON, unknown id, stale version, unsupported media type, and an injected server failure. Predict each outcome before sending it. The goal is not memorizing a status chart; it is learning to communicate distinct outcomes consistently enough that a client can recover without parsing prose.

Active Recall Checkpoint

The client times out after POST and does not know whether creation committed. What must the contract add for a safe automatic retry?

Show answer & explanation

Use an idempotency key or a client-selected stable identifier with documented replay behavior. POST is not idempotent by default, so retry safety must be designed.

Guided Hands-On Lab

Code Walkthrough

Write the HTTP requests a task API needs for list, create, complete, and delete, then choose a status code for each outcome.

TaskApi.http
### 1. Create a Task (POST to collection)
POST http://localhost:8080/tasks
Content-Type: application/json
Accept: application/json

{"title":"Read Module 0"}

# Expected Response:
# HTTP/1.1 201 Created
# Location: /tasks/42
# ETag: "task-42-v1"
# {"id":42,"title":"Read Module 0","status":"OPEN","version":1}

### 2. Retrieve Task by Identifier (Safe & Idempotent GET)
GET http://localhost:8080/tasks/42
Accept: application/json

# Expected Response:
# HTTP/1.1 200 OK
# Content-Type: application/json
# ETag: "task-42-v1"

### 3. Guarded Update with Precondition (Idempotent PATCH with If-Match)
PATCH http://localhost:8080/tasks/42
Content-Type: application/json
If-Match: "task-42-v1"

{"status":"COMPLETED"}

# Expected Response:
# HTTP/1.1 200 OK
# ETag: "task-42-v2"

### 4. Stale Concurrency Update (Fails Precondition)
PATCH http://localhost:8080/tasks/42
Content-Type: application/json
If-Match: "task-42-v1"

{"status":"COMPLETED"}

# Expected Response:
# HTTP/1.1 412 Precondition Failed

### 5. Missing Resource Diagnostic (RFC 9457 ProblemDetail)
GET http://localhost:8080/tasks/999
Accept: application/json

# Expected Response:
# HTTP/1.1 404 Not Found
# Content-Type: application/problem+json
# {
#   "type": "https://example.com/problems/task-not-found",
#   "title": "Task Not Found",
#   "status": 404,
#   "detail": "No task exists with id 999",
#   "instance": "/tasks/999"
# }
POST returns 201 with a Location header, so the client follows the task URI instead of guessing it.
The ETag and If-Match headers give updates a concrete precondition and prevent silent lost updates.
Modern Spring Boot 3 applications communicate failures via RFC 9457 ProblemDetail payloads with proper 4xx/5xx status codes.
Guided LabBuild & Verify Step by Step

The supplied local Task API fixture running on localhost:8080 and TaskApi.http open in IntelliJ HTTP Client or VS Code REST Client.

  1. 01

    Write the resource contract

    Add create, read, complete, delete, and filtered-list requests. Paths identify tasks; filtering and paging go in query parameters.

    Check: Every operation has a deliberate method, success status, content type, and stable resource URI.

  2. 02

    Send the create request

    Run the POST shown above and record the status, Location, ETag, response body, and generated task id.

    Check: The response is 201 Created and Location points at the new task.

  3. 03

    Complete with a precondition

    Send PATCH with the current ETag in If-Match, then replay it with the stale ETag.

    Check: The current version completes the task and the stale request receives 412 instead of overwriting state.

  4. 04

    Fill in the failure matrix

    Send malformed JSON, a blank title, an unknown task id, and an unsupported content type.

    Check: You get deliberate 400, 400, 404, and 415 outcomes instead of one generic 200 or 500.

Command

curl -i -X POST http://localhost:8080/tasks \
  -H "Content-Type: application/json" \
  -d '{"title":"Read Module 0"}'

Expected Output

HTTP/1.1 201 Created
Location: /tasks/<generated-id>
Content-Type: application/json
ETag: "task-<id>-v1"
Simulated terminal replay — matched to lesson verificationJDK 21 / Maven 3.9
spring-boot-lab@localhost:~
$curl -i -X POST http://localhost:8080/tasks \ -H "Content-Type: application/json" \ -d '{"title":"Read Module 0"}'#failingCondition
[INFO] Scanning for projects...
[INFO] -------------------< com.example:order-service >-------------------
[INFO] Building order-service 0.0.1-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- maven-surefire-plugin:3.2.5:test (default-test) @ order-service ---
[INFO] Running com.example.OrderServiceTest
[ERROR] Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 0.284 s <<< FAILURE! -- in com.example.OrderServiceTest
[ERROR] com.example.OrderServiceTest.testOrderCreation -- Time elapsed: 0.041 s <<< ERROR!
java.lang.NullPointerException: Cannot invoke "com.example.OrderRepository.save(Object)" because "this.repository" is null
	at com.example.OrderService.createOrder(OrderService.java:18)
	at com.example.OrderServiceTest.testOrderCreation(OrderServiceTest.java:24)

[INFO] Results:
[INFO]
[ERROR] Errors:
[ERROR]   OrderServiceTest.testOrderCreation:24 » NullPointer Cannot invoke "com.example...
[INFO]
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin:3.2.5:test on project order-service: There are test failures.
[ERROR] -> [Help 1]
[ERROR] BUILD FAILURE
Exit Code: 1
BUILD FAILURE

Synthesis & Production Verification

Deliberate Failure Condition

  1. Break: Return 200 OK with an error message body for failed requests
  2. Observe: Clients cannot retry safely, caches serve failures, and monitoring sees no errors
  3. Repair: Status codes and methods are the machine-readable half of your API contract; JSON is only the other half.

Production Acceptance Criteria

  • Choose between GET, POST, PUT, PATCH, and DELETE by semantics, not habit
  • Pick accurate 2xx, 4xx, and 5xx status codes for outcomes
  • Explain safe, idempotent, and stateless requests with examples

Architectural & Technical Review

Core Interview Questions

Q01What is the difference between safe and idempotent HTTP methods?Show answer

A safe method is intended not to change server state; an idempotent method has the same intended effect when repeated. GET is both, DELETE is idempotent but not safe, and POST is generally neither. These properties determine whether automated retries and prefetching are safe.

Q02When should an API use PUT instead of PATCH?Show answer

Use PUT when the client sends the complete desired representation at a known resource URI; repeating it should produce the same intended state. Use PATCH for a defined partial-change document. A PATCH format can be idempotent, but PATCH does not guarantee idempotency by itself.

Q03How do ETag and If-Match prevent lost updates?Show answer

The server returns a validator such as an ETag with a representation. The client sends that value in If-Match when updating; the server applies the change only if the current validator still matches. A stale value fails with 412 Precondition Failed instead of overwriting newer data.

Production Pitfalls

Critical Anti-Patterns

  • Pitfall: Returning 200 for every outcome — It hides failures from clients, caches, and dashboards. Pick the accurate 2xx/4xx/5xx code for each outcome.
  • Pitfall: Putting verbs in URLs — /getUser?id=1 fights the protocol; the method is already the verb. Design resources (/users/10) and let GET/PUT/DELETE express intent.
  • Pitfall: Omitting 201 and Location on creation — Clients cannot tell whether a POST created something or merely echoed input without an explicit Created status.

Primary Documentation & Specifications