CodeGym /Courses /Java Server /HTTP scenario without contradictions

HTTP scenario without contradictions

Java Server
Level 8, Lesson 4
Available

1. HTTP scenario as a dialogue

When we learn HTTP piece by piece, a typical trap appears: it seems that the method, path, status, and headers are independent “settings” that can be combined however we want. But in the real backend world, HTTP works differently: it is a dialogue where every part must support the meaning of the others. If one part “says” one thing and another says something else, the client starts guessing what happened.

Imagine that an HTTP scenario is a short scene:

the client says: “I perform an action (method) with this resource (path) and, if needed, send data (body), here is how to interpret it (headers)”;

the server replies: “I understood and processed (status), here is what happened (body), and here is how to read it (headers)”.

It is very useful to keep a simple scheme in mind so that you do not drift into “the request body solves everything.”

sequenceDiagram
    participant C as Client
    participant S as Server
    C->>S: "Request: method + path + headers + body"
    S-->>C: "Response: status + headers + body"

If you want a slightly more “programmatic” image, you can mentally describe the scenario as a data structure. Not so that you write server code right now (we are not doing that), but to discipline your thinking: “the scenario has both a request and a response.”


import java.util.Map;

/**
 * One "scenario" = request + response.
 * The idea is that they should be designed as a pair so the parts do not contradict each other.
 */
public record HttpScenario(
        String method,                     // HTTP request method: GET/POST/DELETE...
        String path,                       // Resource path: /api/v1/...
        Map<String, String> requestHeaders,// Request headers: Accept, Content-Type...
        String requestBody,                // Request body (often null for GET)
        int responseStatus,                // Response status: 200/201/204/400/404/...
        Map<String, String> responseHeaders,// Response headers: Content-Type, Location...
        String responseBody                // Response body (null for 204)
) {}

The idea itself is simple: you cannot “design only request” or “design only response.” They are designed as a pair, otherwise you end up with a contract like “it seems to work, but nobody is sure what exactly.”

2. Where to begin: action → method → path

When you look at an HTTP call, it is very easy to jump straight to JSON, because JSON is the most visible part. But the scenario is assembled earlier: first, it is clear what the client wants to do, then the method is chosen, and only after that comes the path. Otherwise, you very quickly end up in a world of “everything through POST” or “everything in query.”

For ReadLater, this basic step is already enough:

- get one item → GET /api/v1/reading-list/42

- get a collection → GET /api/v1/reading-list

- create a new item → POST /api/v1/reading-list

Even at this level, you can already see where the response will most likely go: reading usually leads to 200, creating to 201, and deleting without a useful body to 204. The meaning of the status does not appear out of nowhere — it grows out of the action, method, and path.

3. Where to store request data

After the method and path, the remaining data needs to be placed in the right places. A simple rule helps here:

- path — which exact resource it is;

- query — how to read the collection: filters, search, clarifications;

- headers — a technical agreement about format and processing;

- body — the main payload of the operation.

As soon as the same meaning is spread across multiple places — for example, id is simultaneously in path, query, and body — the request becomes ambiguous. The client and server have to guess what is primary.

4. Headers without magic: Accept and Content-Type

On this framework, headers simply refine the agreement. Accept is the format the client wants in the response. Content-Type is the format of the current body. So for GET without a body, Accept: application/json is often enough, and for POST with JSON you already need Content-Type: application/json and usually the same Accept.

Then headers come into play depending on the situation: Location helps after creating a resource, Allow helps when the path exists but the chosen method is not supported. If you swap Accept and Content-Type, or promise JSON and send something else, the scenario starts arguing with itself before the status is even chosen.

5. Server response: status, headers, body

Once the request is understood, all that remains is to name the result honestly. There is no need to go through the whole set of codes again here: what matters is that status, headers, and body do not contradict one another.

The working minimum for today is this:

- 200 — there is useful data;

- 201 — a new resource was created, often together with Location;

- 204 — the operation succeeded, but there is no body;

- 400, 404, 409, 500 — different reasons for failure, and the client should distinguish them by meaning, not by guessing.

Now let us put all of this together into complete request/response pairs.

6. Complete scenarios: ReadLater

Right now the most useful exercise for the brain is to see whole “scenarios” in full: how a specific operation looks simultaneously in the method, path, headers, status, and the presence or absence of a body. Here, at last, it is not each element separately that matters, but their consistency. We are not writing a server and we are not discussing how this is stored internally — we are training the ability to assemble and read a contract as a single whole. This is the skill that later underpins Postman practice, the Java client, and the server side.

Below are examples for the future ReadLater API (reading list). In the examples, the body will be as simple as possible (sometimes just a “placeholder”), because today we are interested not in the JSON content but in the consistency of the scenario parts.

Reading one item

This scenario seems the simplest, but it is exactly what teaches an important distinction: an invalid identifier format is not “resource not found.” When the client requests /.../abc, the problem is not that the resource is missing, but that the request does not make sense (the identifier must be a number). When the client requests /.../999, the format is valid, but the resource is missing — that is where 404 belongs. And only when the id is correct and the resource exists do we honestly return 200 with a body.

Successful variant (resource found):


REQUEST:
GET /api/v1/reading-list/42
Accept: application/json

RESPONSE:
200 OK
Content-Type: application/json

{ ...item data... }

If you want to think of it “as an object” (to see both request and response at once), you can imagine it like this:


import java.util.Map;

HttpScenario scenario = new HttpScenario(
        "GET",
        "/api/v1/reading-list/42",
        Map.of("Accept", "application/json"),   // Client expects JSON in the response
        null,                                   // For GET, there is usually no request body
        200,
        Map.of("Content-Type", "application/json"), // Server confirms the response format
        """
        { "id": 42, "title": "Clean Code" }
        """                                     // Response body: JSON
);

Invalid id (this is 400, not 404):


REQUEST:
GET /api/v1/reading-list/abc

RESPONSE:
400 Bad Request
Content-Type: text/plain

id must be a number

And that is very logical: the server is not required to look for “abc” in the database, because it is not even a valid key.

Correct id, but the resource is missing (this is 404):


REQUEST:
GET /api/v1/reading-list/999

RESPONSE:
404 Not Found
Content-Type: text/plain

item not found

Notice how in these three variants only what should change changes: the path is basically the same, the method is the same, and the difference in status and message reflects the real cause.

Reading a list with filters

Reading a collection is usually tied to another common beginner mistake: “if the list is empty, then it is 404.” But 404 means “I did not find the resource.” A collection like /api/v1/reading-list exists as a resource even when it currently has zero items. So an empty list is a normal successful 200; the body is just empty (for example, an empty array).

Reading without filters:


REQUEST:
GET /api/v1/reading-list
Accept: application/json

RESPONSE:
200 OK
Content-Type: application/json

{ "items": [ ... ], "count": 2 }

Reading with filters (query params):


REQUEST:
GET /api/v1/reading-list?status=PLANNED&title=clean
Accept: application/json

RESPONSE:
200 OK
Content-Type: application/json

{ "items": [ ... ], "count": 1 }

And here you can see a good use of query: we are not identifying one resource, we are reading a collection and asking to refine the results. If we tried to cram status into the path, we would get something like /reading-list/PLANNED, and it would become unclear: is this a new resource type? is it an id? is it a magic collection name? Query is much more honest in terms of meaning.

Creating an item

Creation is the case where you very quickly see who designed the contract “for show” and who actually thought about the client. If the server created a resource, it is useful for the client to know two things: that creation happened (and not just “some success”), and where that resource can be obtained now. That is why 201 Created and the Location header are especially natural here.

Creation (the client sends data):


REQUEST:
POST /api/v1/reading-list
Content-Type: application/json
Accept: application/json

{ ...new item data... }

Successful response:


RESPONSE:
201 Created
Location: /api/v1/reading-list/43
Content-Type: application/json

{ ...created item, already with id=43... }

You can think of it as a small “template” (without JSON details) to see the consistency:


import java.util.Map;

HttpScenario scenario = new HttpScenario(
        "POST",
        "/api/v1/reading-list",
        Map.of(
                "Content-Type", "application/json", // Format of the sent body
                "Accept", "application/json"        // Format we want to receive back
        ),
        "{ ... }", // Placeholder: this should be JSON, since Content-Type = application/json
        201,
        Map.of(
                "Location", "/api/v1/reading-list/43",   // Address of the created resource
                "Content-Type", "application/json"       // Response body format (if any)
        ),
        "{ ... }" // Placeholder: created resource (often with a new id)
);

And notice: Location here reinforces the meaning of 201. If you set 201 but did not create a new resource, or cannot say where it lives, that already looks suspicious. Not impossible, but suspicious: the client expects creation = a new address appears.

Creation often leads to 409 Conflict as well. Not because “the request is bad,” but because the data conflicts with the current state. For example, in our domain externalId may be required to be unique: you cannot add the same external book to the list twice with the same externalId.


REQUEST:
POST /api/v1/reading-list
Content-Type: application/json

{ "externalId": "OL12345M", ... }

RESPONSE:
409 Conflict
Content-Type: text/plain

externalId already exists

Unsupported method on an existing path

There is another useful negative branch that is not about data and not about a missing resource. The path /api/v1/reading-list exists, but this collection supports reading and creation here. If the client sends DELETE there, this is not 404: the route was found, and the problem is specifically the unsupported method.


REQUEST:
DELETE /api/v1/reading-list

RESPONSE:
405 Method Not Allowed
Allow: GET, POST
Content-Type: text/plain

method not allowed

The Allow header immediately shows the client which methods make sense here at all, so it does not have to guess or try options blindly.

Deleting an item

Deletion is a great test of whether we understand the meaning of 204. If the operation completed successfully, but we do not need to return any data, 204 No Content sounds ideal. It is like saying: “The operation is complete. Do not expect any data.” And it is important not to spoil that meaning: if you chose 204, do not send a body “just in case.” That is like putting a sticker on a box that says “EMPTY” and then slipping in a note that says “but actually not.”

Successful deletion:


REQUEST:
DELETE /api/v1/reading-list/42

RESPONSE:
204 No Content

In Java terms, this can be written almost minimally:


import java.util.Map;

HttpScenario scenario = new HttpScenario(
        "DELETE",
        "/api/v1/reading-list/42",
        Map.of(), // Headers are optional: neither body nor response format matters to us
        null,     // DELETE usually has no request body (in a typical scenario)
        204,
        Map.of(), // 204 = no body, so Content-Type is usually not needed either
        null      // Important: since this is 204, body must really be null
);

And the negative variants again look like GET:

- if the id is not a number, it is 400;

- if the id is a number but the resource does not exist, it is 404.

And that is good: when the rules repeat, the API becomes predictable.

Server errors: 500

500 is a status that is often used incorrectly: either it is applied to everything (“oh, something went wrong”), or people are afraid to use it at all (“let it be 400, that’s scary”). The correct meaning of 500 is very practical: the server itself could not process the request correctly because of an internal failure. The client usually cannot fix this by “slightly changing the request.”

For example, the client made a valid GET /api/v1/reading-list/42, but on the server a NullPointerException happened (yes, that can happen even to smart people before coffee). Then 500 is more honest than trying to pass it off as 404 or 400.


REQUEST:
GET /api/v1/reading-list/42

RESPONSE:
500 Internal Server Error
Content-Type: text/plain

internal server error

Today we are not designing a single beautiful JSON error format — that will be a separate big topic later. But even now it is useful to understand the semantics: 500 is not “a client error,” but a signal that “an incident happened on the server.”

7. Scenario self-check

When scenarios become numerous, it is very easy to produce a “nearly correct” response that actually contradicts itself. It is like telling someone “I cannot talk” and then immediately calling them. Formally both actions are possible, but together they look strange. In HTTP, such contradictions usually show up in small details: the status says one thing, the headers say another, and the body says something else entirely.

The good news is that most contradictions can be caught by a simple mental algorithm: “assemble the scenario from top to bottom.” You can even keep it as a small internal flowchart.

flowchart TD
    A["What is the client doing? (action)"] --> B["Method (GET/POST/PUT/PATCH/DELETE)"]
    B --> C["Path: collection or resource?"]
    C --> D["Data: path/query/headers/body"]
    D --> E["Are Accept / Content-Type needed?"]
    E --> F["What happened on the server? (result)"]
    F --> G["Status (200/201/204/400/404/409/500)"]
    G --> H["Response headers (Content-Type/Location/Allow)"]
    H --> I["Is there a body and does it match the status?"]

And one more simple “contradiction detector” is to look at the most common pairs that must cooperate:

If you have... Check that... Otherwise the client will think...
204 No Content there is really no body “Why are you saying ‘no content’ when content exists?”
Content-Type: application/json the body is really JSON “I tried to read JSON, but it is not JSON”
Accept: application/json the server really returns JSON “I asked for JSON, but got something else”
201 Created a new resource was really created, and Location is useful “Okay, where does it live now?”
405 Method Not Allowed the path exists, but the method is unsupported; Allow is useful “I do not need another path, I need to choose a supported method”
404 Not Found the request is valid, but the resource is missing “I did everything right, the object just does not exist”
400 Bad Request the problem is in the request format/values “I need to fix the request, not wait”
409 Conflict there is a conflict in the data state “The request is fine, but the data does not allow it”
500 Internal Server Error it is really an internal failure “I cannot do anything about this on my side”

This table does not make you a “REST guru,” but it does make you a person who does not slap a contract together blindly. And that is already a huge step.

8. Typical HTTP scenario mistakes

Mistake #1: designing only the body, and remembering the method/status/headers at the end.
It looks like this: “we will have JSON like this... oh right, what was the method? let’s make it POST... and the status... let it be 200.” As a result, the meaning of the operation gets blurred. It is better to first define the action and method, then the path and data placement, and only then think about the body.

Mistake #2: returning 200 OK all the time because “it is a success anyway.”
Yes, 200 is universal, but that exact universality makes it a weak signal. Creating a resource (POST) is much clearer to the client if it ends with 201 Created, and deleting without data is clearer if it is 204 No Content. Status precision is not nitpicking; it is a way to avoid forcing the client to guess.

Mistake #3: sending a body together with 204 No Content.
This is a contradiction in its pure form: you say “there is no content,” and then you still send content. The client library may ignore the body, may try to read it, may behave unpredictably — and all variants will be “your fault,” because you created the conflict yourself.

Mistake #4: confusing Accept and Content-Type.
This confusion is very common: people write Accept: application/json and think they have described the JSON they are sending in the request. But Accept is “what I want to receive,” while the format of the sent body is described by Content-Type. If you mix them up, the server may not understand your body, or the client may not understand the response.

Mistake #5: putting the resource identifier in the query “out of habit” and duplicating it in several places.
A request like /reading-list?id=42 is still something you may see, but as soon as you add /reading-list/42?id=42 and, just in case, id=42 in the body, the contract becomes unreadable. Resource identity usually lives in the path, while query is better left for filters and reading clarifications.

1
Task
Java Server, level 8, lesson 4
Locked
Full HTTP Ticket Creation Scenario
Full HTTP Ticket Creation Scenario
1
Survey/quiz
HTTP Status Codes, level 8, lesson 4
Unavailable
HTTP Status Codes
HTTP codes and headers
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION