CodeGym /Courses /Spring REST & MVC /Designing code, title, and detail

Designing code, title, and detail

Spring REST & MVC
Level 21, Lesson 1
Available

1. status limitations and the role of code

When you first build a REST API, it seems like everything has already been invented for us: there is a status code, so the client will understand. But that is like saying, “We have a house address, why do we need an entrance and an apartment number too?” In a real application, a single 404 is too broad: the client needs to understand which resource was not found and what it should do next — show a “Not found” screen, suggest refreshing the list, or tell the user that, for example, they opened an old link.

The HTTP status is a protocol-level signal; it says “what class of situation this is”: success, client error, server error. It almost never says “what to do in the UI.” That is why we add our own ProblemDetail field on top of it, code — a short, stable scenario identifier. This is something the client can handle like an enumeration, without depending on the message language and without guessing what the server meant in detail.

To make it easier to keep the field roles in your head, it helps to think of them as a “contract between people and machines”:

Field Primarily for whom How stable it should be Example
status for the client at the protocol level stable (obvious) 404
code for client code (frontend/mobile) as stable as possible TASK_NOT_FOUND
title for a person (user/developer) stable Task not found
detail for a person and for debugging the situation may change Task with id 42 was not found

To simplify: status answers the question “is this a success or an error, and from which family,” code answers “which error scenario,” title answers “what should we call it briefly,” and detail answers “what exactly happened right now.” From here on, we are not interested in rehashing the standard fields, but in how not to mix code with them.

2. title and detail: different promises

Here it is important to keep one simple distinction in mind. title is a short and stable label for the class of problem: Task not found, Invalid input, Unsupported media type. detail is the concrete specifics of the current case: which taskId was not found, which value was invalid, which operation failed.

In short: title should be recognizable, detail should be useful. Do not turn title into a log line with dynamic data, and do not turn detail into a second code. That way, it is easy for people to read, and client code does not start latching onto random text.

3. Designing code

code is the very “binding bolt” that makes the API friendly to client code. And here it is important to agree on the style right away; otherwise, in a week you will end up with codes task_not_found, TaskNotFound, TASK-NOT-FOUND, taskNotFound, and one mysterious task404 (which will be “well, I just quickly threw it together”).

In Task Tracker API, we will stick to a simple and very common style: UPPER_SNAKE_CASE, Latin letters only, no spaces, no dynamic content. Not because this is “the canon,” but because it is as convenient as possible for any clients and logs. code is not text for a user. If you want localization into Russian, Spanish, and Klingon, that will be handled on the client side based on code, not by the code itself.

Even more important than style is semantic stability. The same code must not mean different things in different handlers. If today INVALID_INPUT means “DTO validation failed,” and tomorrow you start using that same code for “unsupported media type,” the client will have a lot of fun (especially if it is already shipped to the stores). From the client’s perspective, this will look like the same error, even though it is a different scenario in meaning.

In the project, we predefine the base error codes. They are like the “error dictionary” of your API. The minimum for the course is already set, and that is good: fewer opportunities for creativity where creativity is not needed.

For example, you can define an enum in the api.dto.error package:

package com.example.tasktracker.api.dto.error;

// Base API error codes.
// Clients (frontend/mobile) should handle them as constants.
public enum ApiErrorCode {
    // Names are in UPPER_SNAKE_CASE: stable and convenient for logs/clients.
    TASK_NOT_FOUND,
    INVALID_INPUT,
    MALFORMED_JSON,
    UNSUPPORTED_MEDIA_TYPE,
    INVALID_STATUS_TRANSITION
}

Notice that we are not trying to build an “ideal encyclopedia of all the errors in the world.” We need a small, understandable set. If you feel the urge to add 73 codes “for the future,” that is a normal urge; it is just better to get through it without committing it.

This is the public code dictionary for the project. From now on, strings like "INVALID_INPUT" or "TASK_NOT_FOUND" will mean exactly the values from this dictionary, not some new parallel coding system. And yes: type and code do not duplicate each other — type remains part of the Problem Details standard, while code solves the application-level client logic task.

4. Designing title

title is a short label for the problem that a person should understand at a glance. For our project, a resource-oriented style is more convenient: Task not found, Invalid input, Invalid status transition. It immediately communicates the meaning of the error and still remains stable.

Do not put dynamics into title. Task 42 not found sounds tempting, but that is already detail's job. If title stays stable, it is convenient to see it in Swagger UI, Postman, logs, and tests: one category of problem — one heading.

5. Designing detail

detail is the place for the specifics of the current case. This is exactly where it is appropriate to put taskId, the problematic parameter value, or a brief explanation of what exactly the operation forbids. For example: Task with id 42 was not found or Request body is not a valid JSON.

But detail should not turn into a dump of server internals. Java class names, stack traces, filesystem paths, and raw framework messages only do harm here. The client needs API-contract-level text: specific enough to understand the problem, and external enough not to depend on the internal implementation.

6. Implementation in Task Tracker API

When you start writing @ExceptionHandler, at first everything looks neat. And then you add a second handler, a third, a fourth... and suddenly it turns out that in one place code is called "errorCode", in another "code", and in the third "Code" (because someone accidentally held Shift). That is why it is better to fix one field name right away. In the project, that will be code.

Let us create a small helper method inside our GlobalExceptionHandler (or a separate factory, but at this stage a helper is simpler and clearer). It will assemble the basic public error skeleton {type, title, status, detail, instance, code}, and scenario-specific details will be added on top of it.

import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ProblemDetail;

import java.net.URI;

// Assemble the base public error skeleton in one place:
// standard ProblemDetail fields + our application-specific code.
private ProblemDetail appProblem(HttpStatusCode status,
                                 URI type,
                                 String title,
                                 String detail,
                                 ApiErrorCode code,
                                 URI instance) {
    ProblemDetail pd = ProblemDetail.forStatusAndDetail(status, detail);

    pd.setType(type); // type is a stable identifier for the problem class
    pd.setTitle(title); // title is a short label for a person
    pd.setInstance(instance); // instance is the URI of a specific request
    pd.setProperty("code", code.name()); // code is a stable scenario for client code

    return pd;
}

This is our base builder. fieldErrors, conflicting extension fields, timestamp, and requestId will only supplement this set, not replace it.

Now let us use this helper in the TaskNotFoundException handler. Yes, it is fine for the handler to accept HttpServletRequest — this is normal, because @ControllerAdvice lives in the web layer, and we are not pretending this is the “domain layer”.

import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.web.bind.annotation.ExceptionHandler;

import java.net.URI;

@ExceptionHandler(TaskNotFoundException.class)
public ProblemDetail handleTaskNotFound(TaskNotFoundException ex,
                                        HttpServletRequest request) {
    // We take instance from the request URI so the error can be tied to a specific call
    // We formulate detail ourselves (we do not use ex.getMessage() as part of the external contract)
    return appProblem(
            HttpStatus.NOT_FOUND,
            URI.create("/problems/task-not-found"),
            "Task not found",
            "Task with id " + ex.getTaskId() + " was not found",
            ApiErrorCode.TASK_NOT_FOUND,
            URI.create(request.getRequestURI())
    );
}

Notice the “division of responsibility”: title is stable, detail is specific and depends on the id, code is stable and suitable for client code. And we do not use ex.getMessage() directly — that is a separate discipline: exception messages are usually not intended for the external contract.

7. JSON example and client logic

To feel why all of this is not “bureaucracy” but real value, it helps to see the final JSON through the client’s eyes. For example, GET /api/v1/tasks/42, where the task does not exist, may return:

{
  "type": "/problems/task-not-found",
  "title": "Task not found",
  "status": 404,
  "detail": "Task with id 42 was not found",
  "instance": "/api/v1/tasks/42",
  "code": "TASK_NOT_FOUND"
}

While type/title/detail help the person, the client can branch on code. That means the UI can do logic like this: if TASK_NOT_FOUND, show “Task not found” and a “Back to list” button. If MALFORMED_JSON, show “Check the JSON format.” And the nicest part is that the UI does not have to guess this from detail.

It is useful to imagine how the client application handles this:

flowchart LR
    A[HTTP response] --> B["status 404/400/415"]
    B --> C["parse JSON ProblemDetail"]
    C --> D[read code]
    D --> E["UI logic: switch(code)"]
    C --> F[show title/detail to human]

If code is designed properly, the client-side logic becomes an ordinary branch on a constant, not “searching for a substring in a message.”

And one more example: when invalid input arrives. Until we add fieldErrors (this is the next layer of the contract), the basic response may look like this:

{
  "type": "/problems/invalid-input",
  "title": "Invalid input",
  "status": 400,
  "detail": "Request validation failed",
  "instance": "/api/v1/tasks",
  "code": "INVALID_INPUT"
}

This is already better than “some message string.” The skeleton is the same, client code reads code in the same way, and we are not creating a separate “validation error format.” And we will add the extension with field details later on top of this same skeleton, without breaking the foundation.

8. Typical mistakes: code, title, detail

Mistake #1: making code equal to the error text.
Sometimes a developer writes pd.setProperty("code", "Task with id 42 was not found") because “well, it is a code, and it is a string.” Then code stops being a code and becomes a second detail. As a result, the client cannot rely on it: today the string says “was not found,” tomorrow “not found,” and the day after that you decide to translate it into Russian. code should be a short constant that can be compared strictly.

Mistake #2: putting dynamic details into title.
Task 42 not found seems convenient until the next error appears: Task 43 not found, then Task 44 not found... and you realize that title is no longer stable and cannot be used as a “label for the problem class.” It is much better to keep title constant (Task not found) and move the id into detail.

Mistake #3: using the same code for different meanings.
Today you decide that INVALID_INPUT means “DTO validation,” and tomorrow you start using that same code for “enum conversion error in a query parameter” and “request body is missing.” Formally, all these situations are about a “bad request,” but for the client they are completely different behavior branches. If the codes are mixed, the client is left to look at detail and play “guess the tune.” The codes should distinguish stable scenarios that the client needs to handle differently.

Mistake #4: using ex.getMessage() as detail “as is”.
Exception messages are often unstable, sometimes too technical, sometimes they include internal details. Today ex.getMessage() looks decent, and tomorrow after a library update it may contain a different wording or a fragment that should not be exposed. It is better to formulate detail separately, based on the external contract, even if that seems a bit more verbose.

Mistake #5: forgetting that code is for the whole error, not for an individual field.
Sometimes you want to make code=NotBlank or code=Size and think you have solved the problem. These are good codes, but they refer to specific field validation violations, not to the overall request error. At the top level, code should describe the whole problem class (for example, INVALID_INPUT), while field details should live separately, in extensions. If you mix the levels, the response becomes unstructured: the top-level code starts meaning one field and then another — and the client has to guess again.

1
Task
Spring REST & MVC, level 21, lesson 1
Locked
Stable code for a missing movie
Stable code for a missing movie
1
Task
Spring REST & MVC, level 21, lesson 1
Locked
Unknown export format with an error code
Unknown export format with an error code
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION