CodeGym /Kursy /Spring REST & MVC /Kategorie błędów w API

Kategorie błędów w API

Spring REST & MVC
Poziom 19, Lekcja 1
Dostępny

1. Kategorie błędów i sens

Gdybyśmy pisali aplikację konsolową „dla siebie", można by żyć z filozofią „posypało się – no i trudno". Ale REST API to zawsze interakcja z kimś z zewnątrz: frontendem, aplikacją mobilną, integracją, innym serwisem. I temu „komuś" trzeba rozumieć, co się stało: przysłał złe dane, zażądał nieistniejącego zasobu, trafił na konflikt stanu, czy serwer potknął się o własne nogi.

Największą szkodą wynikającą z braku kategorii jest to, że błędy stają się nierozróżnialne. Klient zaczyna zgadywać po stringu wiadomości, co się stało, albo po prostu pokazuje użytkownikowi „Błąd 500" i odpuszcza. A po stronie serwerowej łapie się potem bugi w stylu „dlaczego UI pokazuje 'serwer niedostępny', gdy użytkownik po prostu zapomniał wypełnić title". Spoiler: bo serwer nie wyjaśnił, że to błąd wejścia.

Kategorie błędów to nie próba zrobienia z ciebie miłośnika tabelek w Excelu. To sposób na uzgodnienie sensu: jeśli błąd dotyczy walidacji, klient wie, że trzeba poprawić żądanie; jeśli błąd jest przedmiotowy, klient rozumie, że żądanie jest poprawne, ale operacja jest zabroniona regułami lub bieżącym stanem; jeśli awaria jest techniczna, klientowi pozostaje tylko powtórzyć później lub pokazać „już się tym zajmujemy", bo problem musi naprawić serwer.

Żeby krótko utrwalić ideę:

Kategoria Co oznacza Kto jest „winny" znaczeniowo Przykład z Task Tracker API
Błąd wejścia (validation / binding / parsing) Żądanie nie odpowiada kontraktowi Klient (lub jego UI) pusty title, błędny enum status, uszkodzony JSON
Błąd przedmiotowy Żądanie poprawne, ale operacja narusza regułę domeny Klient próbuje wykonać zabronione działanie próba zmiany zadania archiwalnego
Awaria techniczna Serwer nie mógł poprawnie wykonać scenariusza z powodu wewnętrznego problemu Serwer/infrastruktura bug w kodzie, nieoczekiwany null, wewnętrzny błąd repozytorium

Dalej czasem będę przez przyzwyczajenie mówić validation error, ale użyjemy tego słowa szeroko: nie tylko o Bean Validation, lecz w ogóle o błędach wejścia/kontraktu – @Valid, parsowanie JSON, binding parametrów i type mismatch.

Tego podziału rozpatrzymy dokładniej, bo „znam trzy słowa" i „umiem klasyfikować w prawdziwym kodzie" to dwa różne wszechświaty.

2. Skąd biorą się błędy w API

Gdy zaczynasz pracę ze Spring MVC, wydaje się, że wszystko dzieje się „w kontrolerze". Jakby żądanie przychodziło i cały świat to twoja metoda create() lub getById(). W rzeczywistości zanim żądanie trafi do kontrolera, dzieje się mnóstwo kroków: parsowane są parametry, czytane jest ciało, konwertowane są typy, uruchamiana jest walidacja. A po kontrolerze – serwisy, repozytoria i reszta dorosłego życia.

Żeby pewnie klasyfikować błędy na kategorie, warto trzymać w głowie ten sam uproszczony pipeline, ale teraz patrzeć na niego jak na mapę przyszłych kategorii, nie tylko jak na listę kroków.

flowchart TD
    %% Uproszczony pipeline przetwarzania żądania w API
    A[HTTP request] --> B["Binding parametrów path/query"]
    B --> C["Odczyt body i JSON -> DTO"]
    C --> D["Bean Validation @Valid"]
    D --> E[Metoda kontrolera]
    E --> F["Warstwa serwisowa: reguły biznesowe"]
    F --> G["Repository/in-memory storage"]
    G --> H[Odpowiedź dla klienta]

    %% Punkty, gdzie najczęściej „pada wejście"
    B -->|type mismatch| X1[Błąd wejścia]
    C -->|malformed JSON| X1
    D -->|constraint violations| X1

    %% Punkty, gdzie najczęściej ujawniają się przyczyny domenowe i awarie serwera
    F -->|not found / conflict / zabronione| X2[Błąd przedmiotowy]
    G -->|awaria przechowalni/bug| X3[Awaria techniczna]
    F -->|nieoczekiwana sytuacja| X3

Zwróć uwagę na ważną rzecz: błąd może się zdarzyć bez wejścia w twój kod. Na przykład klient przysłał status=DOING, a u nas enum TaskStatus tego nie zna. Spring spróbuje przekonwertować string na enum i nie da rady. To nie „błąd logiki biznesowej" i nie „błąd serwisu", to błąd na poziomie wejścia.

To samo z malformed JSON. Możesz pisać ile chcesz if(request == null), ale jeśli JSON jest uszkodzony, do request sprawa nie dojdzie: ciało żądania się nie sparsowało.

Z drugiej strony są błędy, które z zasady pojawiają się tylko w warstwie serwisowej. Na przykład „zadanie jest archiwalne – nie można go zmieniać". Tu wejście może być idealnym JSON i wszystkie ograniczenia mogą przejść. Ale operacja jest nadal zabroniona regułą przedmiotową. To już inna kategoria.

I wreszcie są sytuacje, gdy „wszystko poprawne", ale serwer łamie się sam: bug, nieoczekiwany stan, wewnętrzny błąd przechowalni. To awaria techniczna.

Dalej przejdziemy przez trzy kategorie tak, żebyś mógł nie tylko je nazwać, lecz pewnie wskazać palcem: „tu błąd wejścia", „tu biznes", „tu serwer sam się posypał".

3. Błędy wejścia: gdy żądanie „posypało się na wejściu"

Błędy wejścia to te przypadki, gdy żądania nie można przyjąć w aktualnej postaci. Nie „nie chcemy", nie „mamy taką regułę", lecz właśnie „to nie odpowiada kontraktowi endpointu". Warto myśleć jak programista API: obiecałeś klientowi określoną formę danych i określone dopuszczalne wartości, a klient tę obietnicę złamał.

Tu wpadają dwa bliskie znaczeniowo podtypy: błędy ograniczeń (Bean Validation) i błędy konwersji/formatu (malformed JSON, błędne typy, błędne enumy, nieprawidłowe daty). Z punktu widzenia klienta to jedna i ta sama myśl: „przysłałem niepoprawne żądanie, muszę je poprawić". Tylko źródło inne: gdzieś potknęła się walidacja, a gdzieś wcześniej – parsowanie lub konwersja typów.

Zacznijmy od tego, co już znane: Bean Validation na DTO. Na przykład żądanie tworzenia zadania musi mieć niepusty title o długości od 3 do 120.

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

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

// DTO żądania wejściowego tworzenia zadania.
// Tu utrwalamy kontrakt API: jakie pola są wymagane i jakie ograniczenia na nie działają.
public record TaskCreateRequest(
        @NotBlank // title wymagany: pusty string to błąd wejścia (validation)
        @Size(min = 3, max = 120) // dodatkowo ograniczamy długość: to też część kontraktu endpointu
        String title
) {
}

Jeśli klient prześle title: "" lub title: "ok", to nie jest „konflikt przedmiotowy", nie „serwer nie mógł". To po prostu żądanie nie przechodzi przez kontrakt: z góry powiedzieliśmy, że title ma być taki. Dlatego to typowy błąd wejścia.

Ważny aspekt praktyczny: błędy wejścia żyją na granicy API. Tzn. nie powinno się ich rozsmarowywać po serwisach w stylu if(request.getTitle() == null) throw .... W dobrym projekcie serwis powinien otrzymywać wejście, które już przeszło podstawowe sprawdzenia formy.

Po stronie kontrolera zazwyczaj wystarczy jedno @Valid:

import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

@PostMapping("/api/v1/tasks")
public TaskDetailsResponse create(@Valid @RequestBody TaskCreateRequest request) {
    // @RequestBody: mówimy Spring, żeby przeczytał JSON z ciała i przekonwertował na DTO
    // @Valid: uruchamiamy Bean Validation przed wejściem w logikę biznesową
    return taskService.create(request); // do serwisu trafia już „formalnie prawidłowe" żądanie
}

Teraz o drugiej połowie: błędy formatu i konwersji typów. Tu bardzo łatwo się pogubić, bo „nie wyglądają" jak twoje ograniczenia. Na przykład klient wywołuje list endpoint z filtrem:

GET /api/v1/tasks?status=DOING

Jeśli DOING nie istnieje w twoim TaskStatus, to problem nie leży w logice biznesowej. Serwer po prostu nie rozumie, co mu przekazano, bo wartość nie mieści się w dopuszczalnym zbiorze. To ta sama natura błędu co @Size(min=3), tylko w innym miejscu pipeline.

Kolejny częsty przykład – daty. Jeśli oczekujesz dueBefore=2026-03-01, a klient przysłał dueBefore=01-03-2026 (albo dueBefore=wczoraj – tak, ludzie potrafią), to problem jest w formacie. Serwer nie może poprawnie powiązać stringa z query-parametru z LocalDate. To znowu błąd wejścia.

I osobna „klasyka gatunku" – malformed JSON. Na przykład klient przysłał:

{ "title": "Pay rent", }

Przecinek przed zamykającym nawiasem klamrowym sprawia, że JSON jest niepoprawny i przetwarzanie się przerwie jeszcze przed walidacją. To ważne: nie trzeba próbować „łapać" tego w serwisie. Fizycznie nie dostaniesz DTO – nie udało się go stworzyć.

Podsumowując: błędy wejścia to wszystko, co oznacza „żądania nie można przyjąć w aktualnej postaci, bo narusza ono kontrakt endpointu". Mogą się zdarzyć w ograniczeniach DTO, w parametrach URL, w parsowaniu JSON, w konwersji typów. Ale sens dla API jest jeden: wejście jest nieprawidłowe.

4. Błędy przedmiotowe: poprawne wejście, ale nie wolno

Błąd przedmiotowy (biznesowy) to moment, gdy żądanie wygląda porządnie: JSON się czyta, DTO jest tworzone, walidacja przechodzi, parametry są konwertowane. Klient, jeśli szczerze powiedzieć, może nawet myśleć: „No to teraz serwer musi to zrobić". A serwer odpowiada: „Nie, nie wolno, bo takie są reguły domeny lub bieżący stan zasobu".

Tu szczególnie ważne jest, żeby nie wpaść w „wszystko jest błędem walidacji". Błąd przedmiotowy ma inny sens. To nie „pole zbyt krótkie", lecz „operacja jest w konflikcie z regułami". Właśnie dlatego we wcześniejszych lekcjach wprowadzaliśmy granicę między input validation a business validation: reguły biznesowe zazwyczaj zależą od stanu systemu, a to znaczy że nie można ich w pełni wyrazić adnotacjami na request DTO.

Klasyczny przykład – „zasób nie znaleziony". Załóżmy, że mamy endpoint:

GET /api/v1/tasks/{taskId}

Żądanie może być idealnie skonstruowane, taskId może być stringiem w prawidłowym formacie (nawet poprawnym UUID), ale samego zadania w przechowalni nie ma. To nie błąd walidacji, bo forma żądania jest poprawna. To sytuacja przedmiotowa: klient adresuje zasób, którego nie ma w bieżącym stanie systemu.

W kodzie taka sytuacja powinna być nazwana jawnie, a nie zwracana jako null. Na przykład:

public Task getById(String taskId) {
    // Szukamy zasobu po identyfikatorze: sam taskId może być „prawidłowy", ale danych może nie być.
    Task task = taskRepository.findById(taskId);

    if (task == null) {
        // To przyczyna domenowa (zasób nieobecny), a nie błąd formatu wejścia.
        throw new TaskNotFoundException(taskId);
    }

    return task; // Tu gwarantujemy wywołującemu kodowi, że Task istnieje.
}

Tu pojawia się ważna postać tej modeli: nazwany wyjątek aplikacyjny. TaskNotFoundException mówi czytelnikowi kodu: „to nie jest przypadkowy NPE i nie abstrakcyjne „coś poszło nie tak", to konkretna przyczyna – zadania nie ma". Przydatne, gdy taki wyjątek przechowuje też taskId: ten kontekst potem przyda się dla publicznego error payload.

Inny przykład błędu przedmiotowego – „zadanie jest archiwalne i nie można go zmieniać". To reguła domeny: wejście może być poprawne, ale operacja jest zabroniona. Można to wyrazić tak:

public void assertTaskNotArchived(Task task) {
    // Sprawdzamy regułę domeny: „zadanie archiwalne jest niezmienne".
    // To nie dotyczy formy żądania, lecz stanu zasobu.
    if (task.getStatus() == TaskStatus.ARCHIVED) {
        throw new TaskArchivedException(task.getId());
    }
}

I znowu: żądanie może być idealne, title piękny, tagi unikalne, ale reguła mówi „archiwalnych nie edytujemy". To nie błąd formy żądania, lecz konflikt operacji ze stanem zasobu.

Ważna architektoniczna uwaga: takie sprawdzenia zazwyczaj żyją w warstwie serwisowej (lub domenowej), a nie w kontrolerze. Kontroler powinien być cienki: przyjmuje kontrakt i deleguje wykonanie scenariusza. Jeśli zaczniesz pisać reguły przedmiotowe w każdym kontrolerze, szybko dostaniesz dwie nieprzyjemności: reguły zaczną się duplikować, a zachowanie stanie się niespójne między endpointami.

Kolejny typowy business-konflikt – zabronione przejście statusu. Na przykład jeśli domena mówi, że z DONE nie można wrócić do TODO, to żądanie „zmień status" może być poprawne, ale działanie jest zabronione. W kodzie zamienia się to w sensowną przyczynę:

public void changeStatus(Task task, TaskStatus newStatus) {
    // Przykład ograniczenia domenowego na przejścia statusów.
    // Wejście może być poprawne (prawidłowy enum), ale operacja jest zabroniona regułami.
    if (task.getStatus() == TaskStatus.DONE && newStatus == TaskStatus.TODO) {
        throw new InvalidStatusTransitionException(task.getId(), task.getStatus(), newStatus);
    }

    task.setStatus(newStatus); // Jeśli reguła nie jest naruszona – zmieniamy stan.
}

Tak, ten scenariusz będzie się rozwijać dalej w projekcie, ale jako przykład klasyfikacji jest idealny: wejście poprawne, a reguła zabrania.

Ogólnie błędy przedmiotowe to „nie łam kontraktu żądania, ale nie obiecuj klientowi, że może robić wszystko, co mu się podoba". API to nie tylko forma, lecz i sens operacji.

5. Awarie techniczne po stronie serwera

Awaria techniczna to przypadek, gdy znaczeniowo wszystko powinno zadziałać, ale serwer nie mógł poprawnie ukończyć scenariusza z powodu własnego wewnętrznego problemu. Czasem to bug w kodzie (tak, zdarzają się nawet osobom, które „naprawdę wszystko sprawdziły"). Czasem to problem infrastruktury: nieoczekiwany błąd systemu plików, brak pamięci, awaria serializacji, wewnętrzna niespójność danych.

Warto trzymać w głowie prostą myśl: jeśli klient nic nie może naprawić w żądaniu, bo żądanie jest normalne, i przy tym błąd nie opisuje reguły przedmiotowej, to prawie na pewno jest to problem techniczny serwera.

Najprostszy edukacyjny przykład – naruszenie wewnętrznych niezmienników. Na przykład oczekujesz, że po zapisaniu zadanie zawsze ma id. W naszym projekcie id jest generowane po stronie serwera, klient nim nie zarządza. Jeśli z jakiegoś powodu id nie zostało ustawione, to problem serwera.

public TaskDetailsResponse create(TaskCreateRequest request) {
    // Zapisujemy nowe zadanie. Klient nie zarządza id – to odpowiedzialność serwera/przechowalni.
    Task saved = taskRepository.save(mapper.toModel(request));

    if (saved.getId() == null) {
        // Wewnętrzny niezmienniك jest naruszony: po save() id musi istnieć.
        // To nie błąd klienta i nie reguła domeny, lecz awaria techniczna.
        throw new IllegalStateException("Saved task has no id");
    }

    return mapper.toDetails(saved); // Konwertujemy model na DTO odpowiedzi.
}

Uwaga: to nie „reguła przedmiotowa". Domena nie mówi „zadanie czasem może nie mieć id". To po prostu posypała się nasza wewnętrzna logika. Dlatego rzucamy IllegalStateException: jako programiści stwierdzamy „taki stan jest niedopuszczalny".

Awarie techniczne są szczególnie ważne do odróżniania od błędów przedmiotowych z jednego powodu: nie powinny zamieniać się w „normalną gałąź zachowania". Jeśli zaczniesz pisać kod w stylu „jeśli zdarzy się NullPointerException – zwróć klientowi JSON 'coś poszło nie tak' i żyj dalej", niepostrzeżenie robisz system kruchy i niesprawdzalny. Awaria techniczna to sygnał: „naprawić po stronie serwera".

I jeszcze jeden punkt: awarie techniczne nie powinny być „maskowane" jako walidacja lub błąd przedmiotowy. Czasem bardzo chce się powiedzieć klientowi „no to ty coś nie tak przysłałeś", bo tak łatwiej. Ale jeśli prawda jest taka, że serwer padł z powodu buga, trzeba uczciwie traktować to jako awarię techniczną. W następnej lekcji będziemy mówić o statusach, ale sens jest już teraz taki: różne kategorie błędów powinny mieć różne semantyczne odpowiedzi, inaczej klient zaczyna żyć w trybie „zgadywanki".

6. Przypadki graniczne: szybki algorytm

W praktyce błędy nie przychodzą z tabliczką „jestem walidacją" lub „jestem biznesem". Przychodzą jako fakty: żądanie nie zostało przetworzone, gdzieś rzucono wyjątek, jakiś krok pipeline się posypał. Dlatego przydatny jest mały algorytm klasyfikacji, który można przerobić w głowie bez otwierania filozoficznego traktatu.

Najskuteczniejszy sposób – zadawać sobie pytania w prawidłowej kolejności. Jeśli bardzo uprościć, najpierw sprawdzamy „czy żądanie w ogóle można odczytać i powiązać z kontraktem?", potem „czy można wykonać operację zgodnie z regułami domeny?", i dopiero potem „czy coś nieoczekiwanego zdarzyło się po stronie serwera".

Poniżej – schemat, który wygodnie trzymać w głowie (i tak, można go narysować na karteczce; nikt nie będzie wiedział poza twoim kotem).

flowchart TD
    A[Żądanie przyszło] --> B{"Można odczytać i powiązać z kontraktem?"}
    B -->|"nie: JSON/typy/obowiązkowe pola"| V[Błąd walidacji]
    B -->|tak| C{"Operacja jest dozwolona regułami domeny i stanem zasobu?"}
    C -->|nie| D[Błąd biznesowy]
    C -->|tak| E{"Serwer poprawnie wykonał scenariusz?"}
    E -->|"nie: bug/awaria"| T[Awaria techniczna]
    E -->|tak| OK[Sukces]

Teraz rozpatrzmy najczęstsze „pułapki":

Z malformed JSON jest prosto: jeśli ciało żądania się nie sparsowało, to nie biznes i nie technika. To błąd wejścia, czyli walidacja-znaczeniowo. Tu klient musi poprawić żądanie, inaczej serwer fizycznie nie może zrozumieć danych.

Z brakiem zasobu często się mylą. Wydaje się, że „skoro taskId jest złe – to walidacja". Ale nie: taskId może być zupełnie poprawnym identyfikatorem, po prostu zasobu nie ma. To błąd o stanie systemu. Nie o formie żądania, lecz o braku danych.

Z zakazami w stylu „archiwalnego nie można zmieniać" też często się mylą, szczególnie nowicjusze przyzwyczajeni do „wszystko walidujemy adnotacjami". Ale ta reguła nie dotyczy struktury wejścia, lecz sensu operacji na bieżącym stanie. Znaczy – błąd przedmiotowy.

A awaria techniczna – to coś, co nie mieści się ani w „złe wejście", ani w „reguła domeny zabrania". Jeśli masz nagle IllegalStateException oznaczający „wewnątrz systemu niemożliwy stan", to nie jest powód obwiniać klienta. To powód naprawiać kod.

Przydatna „ściąga" według oznak (bez zamieniania w listy): jeśli błąd wskazuje na konkretne pole żądania i jego dopuszczalność, to prawie zawsze walidacja. Jeśli błąd mówi o konkretnym zasobie, jego istnieniu i stanie, to częściej biznes. Jeśli błąd mówi „niemożliwy stan", unexpected, null tam gdzie nie powinno być null, IO error – to technika.

Gdy taka klasyfikacja jest zebrana, wybór statusu przestaje być zgadywanką: 400, 404, 409 i 500 w końcu mają oparcie w sensie, a nie w nastroju autora kontrolera.

7. Nazwane wyjątki i kategorie w kodzie

Bardzo częsty problem projektów początkujących – wyjątki bez sensu. Na przykład throw new RuntimeException("Bad request"). Taki string w kodzie nic nie wyjaśnia: nie wiadomo, czy to o walidacji, o regule przedmiotowej, o braku zasobu, czy o wewnętrznym bugu. W efekcie ginie kategoria błędu, a razem z nią właściwy status HTTP i normalna forma odpowiedzi.

Nazwane wyjątki aplikacyjne rozwiązują ten problem: zamieniają „coś poszło nie tak" w konkretną przyczynę, którą można konsekwentnie obsługiwać. W ramach naszego kursu żyją one w pakiecie com.example.tasktracker.domain.exception, bo głównie wyrażają przyczyny domenowe (not found, archived, conflict). To ważny podział: DTO i walidacja – w api, a sensowne przyczyny – w domain.

Przykład zadanie nie znalezione u nas wygląda już jak nazwany wyjątek aplikacyjny z taskId w środku. Tu ważna jest sama kategoria: to przyczyna domenowa zasób nieobecny.

Wewnątrz takiego wyjątku wiadomość może być na razie „surowa". I to normalne. Teraz ważne co innego: przyczyna dostała nazwę i w kodzie nie pomylimy jej z „zadaniem archiwalnym" lub „awarią techniczną".

A oto przykład wyjątku „zadanie jest archiwalne":

package com.example.tasktracker.domain.exception;

// Przyczyna domenowa: operacja jest zabroniona z powodu stanu zasobu (ARCHIVED).
public class TaskArchivedException extends RuntimeException {
    public TaskArchivedException(String taskId) {
        // Wiadomość – wewnętrzna; później będziemy mapować przyczyny na jednolity format błędów API.
        super("Task is archived: " + taskId);
    }
}

Teraz TaskNotFoundException i TaskArchivedException – oba o domenie, ale sensy różne. I to już pomaga klasyfikować: not found – brak zasobu, archived – zakaz operacji z powodu stanu zasobu.

Są dwie ważne reguły, które pomagają nie zamienić wyjątków w zoologiczny ogród.

Reguła pierwsza: nie trzeba tworzyć wyjątku na każdą linię kodu. Nie piszemy encyklopedii wyjątków, utrwalamy stabilne przyczyny, które naprawdę mają znaczenie dla zewnętrznego kontraktu API. Task not found – ważna przyczyna. Mapper got null task – najczęściej wewnętrzny bug i może pozostać wyjątkiem technicznym.

Reguła druga: wyjątek powinien wyrażać sens, a nie strategię odpowiedzi. Tzn. TaskNotFoundException – dobra nazwa, a Task404Exception – zła. Dlaczego? Bo nie doszliśmy jeszcze do mapowania statusów, a już wciągnęliśmy HTTP do warstwy domenowej. Przyczyny domenowe powinny pozostawać przyczynami domenowymi; HTTP – to warstwa transportu, którą zastosujemy na wierzchu.

I tak, błędów walidacji generowanych przez Bean Validation lub binding Spring MVC zazwyczaj nie opisujemy swoimi wyjątkami. Są już „nazwane" na poziomie frameworka. Nasze zadanie – rozumieć ich kategorię i później doprowadzić do jednolitego modelu odpowiedzi, ale nie mnożyć ręcznej obsługi w kontrolerach.

8. Typowe błędy przy klasyfikacji błędów

Błąd nr 1: uważać, że „każdy błąd = walidacja".
Wygląda kusząco: nakładamy adnotacje, a wszystko inne nazywamy „invalid request". Ale w efekcie klient nie odróżnia „masz pusty title" od „zadania nie istnieje" ani od „archiwalnego nie można zmieniać". API staje się nieczytelne: niby wszystko 4xx, ale sensu zero.

Błąd nr 2: nazywać brak zasobu błędem walidacji.
Jeśli żądanie jest poprawne, ale danych w systemie nie ma, to nie „zły format", lecz stan domeny. Nie możesz tego naprawić „trochę zmieniając JSON". Klient powinien rozumieć, że adresowanego zasobu nie ma, a nie że „nie przeszedł constraints".

Błąd nr 3: chować konflikty przedmiotowe w awariach technicznych.
Gdy reguła domeny zabrania operacji, a ty zamiast tego rzucasz ogólny RuntimeException i dostajesz „błąd serwera", klient będzie myślał że „serwer się posypał", choć w rzeczywistości prosił o zrobienie czegoś zabronionego. To psuje UX i utrudnia debugowanie scenariuszy.

Błąd nr 4: podawać wewnętrzny bug jako problem klienta.
Czasem chce się „uspokoić" system i zwrócić „nieprawidłowe żądanie", gdy naprawdę masz NullPointerException z powodu buga. Tak pozbawiasz się szansy na normalne naprawienie problemu: w logach będzie chaos, klient będzie zgadywał jak „prawidłowo wysłać żądanie", a serwer będzie padać dalej.

Błąd nr 5: używać jednego wspólnego wyjątku BusinessException na wszystko.
Niby zrobiłeś krok naprzód – oddzieliłeś biznes od techniki. Ale jeśli wewnątrz koszyka biznesowego leżą i not found, i archived, i invalid status transition, i comment not found, znowu tracisz sens. Nazwane wyjątki istnieją właśnie po to, żeby sens był rozróżnialny.

1
Zadanie
Spring REST & MVC, poziom 19, lekcja 1
Niedostępne
Klasyfikator kategorii według typu wyjątku
Klasyfikator kategorii według typu wyjątku
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION