CodeGym /Courses /Java Server /API contract: a request-and-response agreement

API contract: a request-and-response agreement

Java Server
Level 9, Lesson 3
Available

1. The role of the API contract, even when “it is obvious anyway”

If you have ever tried to agree on “let’s just call each other and figure it out as we go,” then you are already familiar with the concept of “there is a contract, but it lives in people’s heads and everyone has their own version.” In the HTTP world, this almost always ends with the client “expecting one thing,” the server “thinking another,” and logs standing between them, sadly whispering: “400.” An API contract is a way to stop guessing and start agreeing.

When we say “API contract,” we do not mean a legal document or stamps marked “approved” (although sometimes it feels like servers will not listen without an RFC stamp either). In practical backend terms, a contract is a set of rules that both client and server understand in the same way: which method to use, which path to hit, which parameters and headers are required, which statuses mean success or failure, and what shape the response has.

Why is it bad to have no contract, even in small educational projects? Because you very quickly end up in the “it worked yesterday” situation. You changed the path, but the client keeps calling the old one. Or the server started returning 200 OK with the text “error,” and the client happily treats it as success. Or you expect JSON, but get an HTML page with the message “Service unavailable” (and yes, that is real life, not a science-fiction plot). A contract is what keeps the system from turning into a spontaneous improvisation theater.

Important: a contract is not “what the server returned today.” One lucky response is just luck. A contract is what must remain predictable from request to request and from day to day.

2. What an API contract consists of

In the previous lessons, we broke request/response down “piece by piece,” and now those pieces suddenly form a very practical idea: a contract is not one chunk (not only the body and not only the status), but a bundle. If you remove one element, the agreement becomes leaky. It is like saying “come over” without giving the address: the phrase exists, but it will not work.

Below is a handy “contract map.” This is not an exam checklist; it is simply a way to keep in mind what exactly the client and server must understand in the same way.

Contract part Where it lives Example (human-friendly) Why it matters
HTTP method request line GET or
POST
The method defines the expected effect: read or change state
Path URL /api/v1/reading-list The path defines the resource; change the path, and you effectively change the address
Query params URL ?status=PLANNED Filters and “reading modes”; their names and values are also part of the agreement
Request headers headers Accept: application/json The client tells the server what it expects; the server can change behavior
Response statuses response status 200,
404
,
409
This is the semantic fork for the client: success/failure/conflict
Response headers response headers Content-Type: application/json Without them, the client may not understand the body format
Response body (shape) response body “an object with a list + count” The data shape must be stable, otherwise the client breaks
Errors (shape) status + body “always JSON with code and message” Errors are also part of the contract, not random text

Notice the subtle point here. When we say “response body shape,” we do not have to dive into detailed JSON design right now (that will be a separate level). But even today, you can understand the difference between “the response must be JSON” and “the response is something unclear, sometimes JSON, sometimes a string, sometimes complete silence.” Even without knowing all the details of JSON, you can already think in contract terms: the response has an expected format, and it must be respected.

3. Shape and values: contract violations

When a beginner first hears the word “contract,” fear often appears in their head: “so nothing can be changed at all?” It can. Moreover, response data changes constantly. That is the whole point of an API: it returns current information. A contract is not a ban on life; it is an agreement about structure and meaning.

Let us divide changes into two types.

The first type is changes in values. For example, yesterday the book “Clean Code” had a rating of 4.7, and today it is 4.8. Or the reading list was empty, and then it became three items long. That is normal data life. The contract does not break because the shape is the same: “there is a rating field as a number,” “there is an items array.”

The second type is changes in shape. For example, instead of items, the server started returning data. Or instead of an array, it began returning a string. Or it changed GET /books/search to POST /searchBooks (yes, that is how APIs are sometimes “improved”). That is already a contract change, and the client has every right to “break” — it is not required to guess the new reality.

Another popular contract violation is when the status says one thing but the body says another. For example, the server returns 200 OK, but the body says “error: invalid token.” A person can still understand that by eye, but the client sees it as success because the status is the main signal. We will come back to this, but let us note it now: the status and the body shape must be consistent.

To feel this “with your hands” (even though we are not writing an HTTP client yet), you can think of the contract as an “expected template.” Suppose we expect a successful response to look like this in principle:

— status 200 OK;
— Content-Type says JSON;
— the body contains the expected keys.

And even in plain Java, you can express this idea as a tiny shape check, without tying it to specific values:

boolean looksLikeListResponse(String body) {
    // A very rough "shape" check, not a value check.
    // Important: this is NOT JSON parsing — it is just a quick sanity check that the response resembles the agreement.
    return body.contains("\"items\"") && body.contains("\"count\"");
}

Yes, this is not “real JSON parsing” (we will do that later with Jackson), but the idea is useful: we check not how many elements there are, but whether the response at least resembles what we considered the agreement.

4. Errors are part of the contract: 404 by the rules

Here it is enough to keep one anchor from the discussion about networking: a contract only makes sense where a response has actually been received. If there is no response, we stay on the network-failure branch and do not reach the status. But once there is a response — even an error response — it must follow the contract.

In a healthy API, errors are not “something went wrong, have some text,” but part of the agreement. When the client receives 404, that does not mean “the server died.” It is a normal contract branch: the server answered that the resource was not found. The client can handle that as an expected situation rather than a catastrophe.

But if the server returns 200 with the body “Not Found” instead of 404, the contract becomes unreadable: the status says success, but the meaning says no. Likewise, if the client did not send a required parameter and the server tries to “guess” and return something strange, that also breaks the agreement. A contract is useful precisely because it defines where guessing ends and clear rules begin.

It is also important to agree on at least the principle that errors must have a predictable shape. We are not designing the final error JSON for our future local API right now (that will be a separate block in the course), but you can already understand the idea: if you return errors, it is better to return them in one format. Otherwise, the client needs ten different handlers: one for strings, another for JSON, a third for HTML, a fourth for “oops, empty body.” Such a client becomes nervous and looks at the internet suspiciously.

A good mental picture looks like this:

flowchart TD
    A[Client sent a request] --> B{"Did a response arrive?"}
    B -- no --> C["Network failure: no status, no body"]
    B -- yes --> D{"Status code"}
    D -- 2xx --> E["Success: verify that the response matches the contract shape"]
    D -- 4xx/5xx --> F["Error: this is also a contract branch, and it must be predictable"]

This diagram is not something you should memorize. It is there so you do not begin analyzing a problematic call with “well, there is something written in the body.” If there is no response, you cannot read the body. If the status is an error, that may be a normal branch. If the status is successful but the shape is broken, that is a contract violation.

5. How contracts are recorded

Even if you perfectly understand that a contract is an agreement, one funny (and sad) question remains: “where is that agreement stored?” If the answer is “in my head,” congratulations, that is the most unstable storage in the world. It crashes every vacation and sometimes after lunch.

In real teams, contracts are recorded as artifacts. Sometimes it is documentation, sometimes a table, sometimes a specification, sometimes request/response examples, sometimes a request collection in a tool. In our course, we will gradually move toward a very practical scheme: you will not only “hold” the contract in theory, but also study it in a tooling-oriented way (later, Postman and “sample responses” will appear to keep the course stable). But right now, the important thing is the principle: the contract must be explicit and verifiable.

Explicit means you can show another person: here is the method, here is the path, here is the required query parameter, here are the expected statuses. Verifiable means you can reproduce the request and see that the server responds as promised. And yes, “I remember that it responded like that” is not verification. That is just memory. A contract is valuable because you can re-check it a month later, when you have already forgotten the details.

There is another clever reason to like an explicit contract: it protects you from “accidental” changes on the side of an external service. In the ReadLater Starter project, we will have an external book catalog. We do not own that service. It may be slow, it may change, it may be unavailable. If we do not have an explicit contract (and examples), then every failure turns into an investigation: “is this how it should be, or is everything broken?” With a contract, at least you understand: “by agreement, it should look like this; if it does not, that is a problem.”

6. A mini contract in code: Java structures

While we are not writing an HTTP client yet and not spinning up a server, we can already adopt a useful habit: do not smear contract knowledge across strings everywhere. Even in a learning project, it is convenient to keep key contract elements in one place: paths, query parameter names, expected media types. This is not “architecture for growth”; it is simply a way to avoid getting ten different strings like /health, /Health, and /heath in the code (the last one is especially good).

Suppose we want to fix a piece of the future contract for the local ReadLater API: endpoint /health. We are not implementing the server yet; we are only defining “what it should be called.”

package com.example.readlater.app;

public final class ReadLaterApiContract {
    private ReadLaterApiContract() { }

    // Base path for checking that the service is alive (health check)
    public static final String HEALTH_PATH = "/health";

    // API version prefix: convenient for changing the version centrally
    public static final String API_PREFIX = "/api/v1";

    // Full path to the reading list resource (built from the prefix to avoid duplicating strings)
    public static final String READING_LIST_PATH = API_PREFIX + "/reading-list";
}

This looks like a small thing, but in a couple of weeks you will thank yourself: when you start writing handlers and client calls, you will not have “magic strings” all over the code.

Now let us take one more step: let us try to describe an endpoint as a small “spec.” No overengineering, literally 6–8 lines.

package com.example.readlater.app;

public record EndpointSpec(
        // HTTP method: GET/POST/... (part of the contract)
        String method,
        // Path (URL without host): for example, /health or /api/v1/reading-list (part of the contract)
        String path,
        // What the client expects to receive: for example, application/json (part of the contract)
        String accept,
        // Which status code we consider successful at this point of the contract
        int successStatus
) { }

And an example of use (even without real HTTP):

package com.example.readlater.app;

public class ContractDemo {
    public static void main(String[] args) {
        // We fix the minimal "contract expectations" for /health
        EndpointSpec health = new EndpointSpec(
                "GET",
                ReadLaterApiContract.HEALTH_PATH,
                "application/json",
                200
        );

        // Just show that this can already be stored/logged/compared
        System.out.println(health); // EndpointSpec[method=GET, path=/health, accept=application/json, successStatus=200]
    }
}

The point is not to “describe everything in code.” The point is to feel that a contract is not an abstraction. It can be represented very concretely. And when later you write an HTTP client using HttpClient or a server using HttpServer, you will know exactly what you are implementing: method, path, headers, expected status.

And now the most useful part: a contract helps you write a simple check that says “did we even reach the place we intended to reach?” For example, you have method+path and want to make sure the request at least resembles the contract at the shape level.

package com.example.readlater.app;

public class ContractMatcher {
    public static boolean matches(EndpointSpec spec, String method, String path, String accept) {
        // A direct comparison: useful as a first approximation.
        // In real code, normalization is often added (method case, spaces, default values).
        return spec.method().equals(method)
                && spec.path().equals(path)
                && spec.accept().equals(accept);
    }
}

This is primitive — and that is fine. We are not building a framework right now. We are training a muscle: “a contract can be formalized, and therefore we can stop guessing.”

7. The contract in ReadLater Starter: the catalog and the local API

So that the contract does not remain “about someone somewhere on the internet,” let us apply it to our end-to-end project. ReadLater Starter will have two major phases: first we talk to an external book catalog, then we bring up a local API. The contract-centric view works the same way for both phases: we define what exactly we consider a correct request and a correct response.

Let us take a maximally abstract but realistic contract for searching in a book catalog. Here, method, path, query, and expected statuses matter, without tying it to a specific provider at the byte level.

Contract example (external catalog, search):

# This is not a "raw HTTP packet," but a brief description of the expected contract.
METHOD:   GET
PATH:     /catalog/search
QUERY:    q=<search string>, limit=<number> (optional)
HEADERS:  Accept: application/json
SUCCESS:  200 OK + JSON (including an empty list if there are no matches)
ERRORS:   400 (bad request), 404 (wrong path or request to a missing specific resource), 500 (service error)

For a search scenario, the absence of results often remains a successful response: the endpoint exists, but the collection is empty.

Now an example of a local API. The simplest and most useful endpoint is /health.

Contract example (local API, health):

# Classic HTTP request example: method + path + protocol version and headers.
GET /health HTTP/1.1
Host: localhost:8080
Accept: application/json

Expected response:

HTTP/1.1 200 OK
Content-Type: application/json

And a body “in some JSON shape.” For now, it is enough to understand that this is not HTML, not a random string, and not emptiness. Later, we will agree on the exact structure.

To see the contract as a whole, it helps to keep it in a “method + path + meaning” table. For example, for the future reading list API:

Operation Method Path Meaning (very short)
Check liveness GET /health The server is alive and returns a status
Get the list GET /api/v1/reading-list Read the collection
Get by id GET /api/v1/reading-list/{id} Read a single resource

Even without an implementation, one important thing is already visible: the path and method are part of the agreement, and the implementation must then obey that agreement. Otherwise, you will start “accidentally” doing POST /reading-list/getById and wondering why everything looks strange.

8. Typical mistakes when thinking about an API contract

Mistake #1: thinking that the contract is only the JSON body.
This is a very common trap: a person looks at a pretty response and forgets that the contract starts with the method and path, continues with headers and status, and only then reaches the body. As a result, Content-Type or the status changes, the client breaks, and “bad JSON” gets blamed, even though the problem was at a completely different level.

Mistake #2: “if the status is 200, then everything is fine.”
200 OK means only that the server claimed success. If the response body does not match the agreed shape, or the server returns an error message inside a “successful” body, then that is not success — it is a contract violation. The client has to guess what to trust: the status or the body, and both options are bad.

Mistake #3: mixing up “no response” and “response with an error.”
When a service is unavailable or the network gave no response, you do not have an HTTP status. That is not 404 and not 500. It is a different class of problem, handled differently. If you call any failure “500,” you lose important information and make diagnostics harder: you do not understand where the error is — in the network or in the application.

Mistake #4: changing the method/path/status and not counting it as a contract change.
Sometimes it seems: “so what, we renamed /books to /book.” For the client, this is not “so what”; it is a different address, and the old call will stop working. A contract is not an internal detail of the server; it is the “external interface.” Changes to it must be treated seriously.

Mistake #5: keeping the contract only in developers’ heads.
If the contract has no explicit representation (at least in the form of a table, examples, notes, or a set of requests), it will start to drift. One developer “remembers” that Accept is required; another thinks it is not. One expects 201; another always returns 200. In the end, you spend time not on code, but on reconstructing agreements after the fact.

1
Task
Java Server, level 9, lesson 3
Locked
Does the request match the contract
Does the request match the contract
1
Task
Java Server, level 9, lesson 3
Locked
Contract Response Validator
Contract Response Validator
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION