JavaRush /Курси /Spring REST & MVC /Семантика list endpointʼа

Семантика list endpointʼа

Spring REST & MVC
Рівень 22 , Лекція 4
Відкрита

1. Семантика: порожньо, 404 і 400

Дуже легко сплутати ці три сценарії, бо зовні вони всі схожі на «ну… щось не вийшло». Але для API це різні історії, і кожна має мати свій статус та свій формат відповіді. Уявіть собі бібліотеку: GET /tasks — це «покажи каталог», а GET /tasks/{id} — це «дай конкретну книжку з полиці за інвентарним номером». Якщо книжок у каталозі немає, бібліотека все одно існує. А от якщо ви просите книжку за номером, а її немає, — це вже інша ситуація.

Для collection endpointʼа (у нашому випадку /api/v1/tasks) порожній результат означає лише те, що на поточний запит немає елементів. Це не «ресурс не знайдено», бо ресурс «колекція задач» як концепція існує завжди: зараз у ньому просто нуль елементів або на вибраній сторінці немає жодного запису. Тому в нормальному REST API це залишається 200 OK і валідним PagedResponse, де items — порожній список.

404 Not Found у цьому випадку — це коли ви адресували конкретний ресурс, а його немає. Наприклад, GET /api/v1/tasks/{taskId} для неіснуючого taskId. Або коли ви взагалі звертаєтеся не до того шляху, тобто не до того endpoint.

400 Bad Request — це коли запит не проходить як запит: ви надіслали відʼємне значення для сторінки, розмір сторінки 0, надто велике значення size, сортування за недозволеним полем, напрямок upsideDown або page=abc. І ось тут найважливіше: 400 — це не «нічого не знайдено», а «запит невалідний».

Щоб зафіксувати це не лише словами, корисно тримати в голові невелику матрицю:

Ситуація Приклад запиту Що повертаємо Чому так
Валідний запит, але елементів немає GET /api/v1/tasks?page=10&size=20 200 OK + PagedResponse з порожнім items Колекція існує, просто на цій сторінці порожньо
Ресурс не знайдено GET /api/v1/tasks/{taskId} 404 Not Found + ProblemDetail Конкретного ресурсу за адресою немає
Запит невалідний GET /api/v1/tasks?size=0 або
sort=hack,asc
400 Bad Request + ProblemDetail Клієнт порушив контракт параметрів

Зверніть увагу, що в першому рядку таблиці ми не пишемо «помилка», хоча користувачу може бути прикро побачити порожню відповідь. Але API створене не для емоцій, а для передбачуваності.

Це розрізнення варто тримати жорстко від самого початку: набір параметрів списку ще зростатиме, а статусна семантика колекції не має розмиватися.

2. 200 OK і порожній items

Порожній результат — це не «особливий випадок, який треба обробляти окремою гілкою в контролері». Це нормальна гілка виконання list endpointʼа. І в цьому є практичний сенс: клієнт може показати порожній стан — «поки задач немає» — і не вмикати аварійну логіку обробки помилок. Помилка — це коли порушено контракт, а не коли даних немає.

У нашому контракті порожній результат має виглядати так само стабільно, як і непорожній: тіло відповіді — це все той самий PagedResponse<TaskSummaryResponse>, просто items порожній. І ось тут важлива дисципліна: items має бути списком, а не null. null — це подарунок клієнту у вигляді NPE/Cannot read property 'length' of null і дивних ifʼів на фронті. Порожній список — це «мені нічого показати», а null — це «я взагалі не знаю, що це за поле».

Є два «порожні» сценарії, які важливо розрізняти в голові, хоча не обовʼязково розрізняти їх статусами:

Перший — порожня колекція. Це коли totalElements == 0, тобто задач узагалі немає. Тоді items порожній, totalElements = 0, а totalPages за вибраним нами правилом теж буде 0. Так, хтось робить totalPages=1 навіть за нуля елементів, але тоді метадані перетворюються на філософію. У нашому навчальному проєкті логіка простіша: сторінок немає, бо немає елементів.

{
  "items": [],
  "page": 0,
  "size": 20,
  "totalElements": 0,
  "totalPages": 0,
  "sort": "updatedAt,desc"
}

Другий — порожня сторінка за непорожньої колекції. Це коли елементи в принципі є, але вибрана сторінка «дуже далеко за горизонтом». Наприклад, у нас усього 7 задач, size=5, отже сторінок 2 (0 і 1), а клієнт запросив page=10. Це не помилка контракту: page не порушує діапазон, бо ми не вводили max page. Запит валідний, просто результат порожній.

{
  "items": [],
  "page": 10,
  "size": 5,
  "totalElements": 7,
  "totalPages": 2,
  "sort": "updatedAt,desc"
}

Чому це корисно? Бо клієнт може чесно зрозуміти ситуацію без вгадування. Він бачить: totalPages=2, page=10 — отже, запитано надто далеко, і треба перестати гортати. Якби ми замість цього повертали 404 або 400, клієнту довелося б вирішувати, це «помилка», «немає даних» чи «шлях не знайдено».

Окремо важливо, щоб відповідь відображала реально застосовані page, size і sort. Якщо клієнт не надіслав sort, а ми застосували значення за замовчуванням, то у відповіді має бути sort: "updatedAt,desc". Це не «дублювання», а спосіб прибрати двозначність: клієнт бачить не лише дані, а й контекст, за якими правилами їх було отримано.

3. 400 Bad Request для page/size/sort

Порожній результат — це про дані. 400 Bad Request — це про контракт. І саме тут починаються найулюбленіші помилки початківців: «давайте просто підправимо вхідні параметри й зробимо вигляд, що все добре». Наприклад, якщо клієнт надіслав size=1000, сервер тихо робить size=100, щоб «не сваритися». Звучить дружньо, але насправді ви створюєте приховану магію, через яку клієнт не розуміє, що робить неправильно, а потім це вилазить у вигляді багів: «у нас іноді повертається не та кількість елементів».

Ми домовилися про просте правило: відсутній параметр — це привід узяти значення за замовчуванням. Невалідний параметр — це привід повернути 400 через наш уже наявний error contract на базі ProblemDetail.

На вході до list-endpoint уже є явний TaskSearchCriteria: page допускає тільки значення >= 0, size — діапазон 1..100, а sort проходить окрему перевірку формату та дозволеного переліку. Тут важливо не повторювати весь код ще раз, а втримати сенс: null означає «параметр не прийшов», а будь-яке порушення діапазону або дозволеного переліку — це invalid input. Саме такий DTO потім спокійно переживає розширення списку новими умовами відбору: змінюється вміст criteria, а не статусна логіка endpointʼа.

Тепер сценарії виглядають так. Якщо page не прийшов, він null, і наша нормалізація спокійно перетворює його на 0. Якщо page=-1, це вже порушення контракту, і воно має піти в 400. Якщо size=0 — теж 400. Якщо size=1000 — також 400. Ми нічого не «лагодимо» мовчки, бо контракт має бути зрозумілим.

Із sort трохи цікавіше: Bean Validation тут не вирішує все, бо формат field,dir ми розбираємо вручну, і whitelist теж перевіряємо вручну. Тому помилку на кшталт «непідтримуване поле сортування» ми перетворюємо на наш доменний виняток «invalid input» — наприклад, InvalidInputException — і далі віддаємо ProblemDetail через @ControllerAdvice. Невідоме поле, неправильний напрямок або зламаний формат — це 400, а не порожній список і не тихий fallback на значення за замовчуванням.

Ключова ідея цієї лекції: помилка в sort — це не «нічого не знайдено» і не «поставимо значення за замовчуванням та промовчимо». Це невалідний запит, і він має приводити до 400 в єдиному форматі помилок.

А ще важливо памʼятати про типи. Якщо клієнт надіслав page=abc, то це не «не пройшов @Min», бо до Bean Validation справа може не дійти: Spring спочатку намагається сконвертувати параметр запиту в число, і це падає на етапі звʼязування або перетворення типів. Але з погляду зовнішнього контракту це все одно invalid input. Тому наш глобальний error layer має перетворювати і validation errors, і type mismatch, і «unsupported sort» у передбачуваний ProblemDetail.

Приклад відповіді (спрощений, але за змістом правильний) на size=0:

{
  "type": "about:blank",
  "title": "Невалідний запит",
  "status": 400,
  "detail": "Перевірку не пройдено",
  "instance": "/api/v1/tasks",
  "code": "INVALID_INPUT",
  "fieldErrors": [
    {
      "field": "size",
      "message": "має бути більшим або дорівнювати 1"
    }
  ]
}

І приклад на «непідтримуване поле сортування»:

{
  "type": "about:blank",
  "title": "Невалідний запит",
  "status": 400,
  "detail": "Непідтримуване поле сортування: hackerField",
  "instance": "/api/v1/tasks",
  "code": "INVALID_INPUT"
}

Зверніть увагу, як це допомагає клієнту. У першому випадку він може підсвітити конкретне поле size. У другому — показати користувачу, що не можна сортувати за цим полем, або виправити UI, наприклад прибрати заборонене поле зі списку. Якби ми повертали 200 і порожній список, клієнт не зрозумів би взагалі нічого.

4. Приклади .http для 200/400/404

Коли ви проєктуєте семантику, дуже корисно перевіряти її не лише очима коду, а й очима клієнта. У нашому проєкті це зручно робити через .http файли (або Postman), бо там добре видно: запит → статус → тіло. І відразу стає зрозуміло, де у вас «все однаково і незрозуміло», а де клієнт справді отримує сигнал.

Ось приклад валідного запиту на «далеку» сторінку. Він має бути успішним, але з порожніми items:

### Порожня сторінка: запит валідний, items порожні
GET http://localhost:8080/api/v1/tasks?page=10&size=5&sort=updatedAt,desc
Accept: application/json

# Очікуємо: 200 OK (навіть якщо items = [])

Якщо задачі існують, ви побачите 200 OK і items: [], а також чесні totalElements/totalPages. Якщо задач узагалі немає, ви все одно побачите 200 OK, просто totalElements буде 0.

Тепер приклад невалідного розміру сторінки. Це вже не «порожньо», а «зламали контракт»:

### Невалідний size: має бути 1..100
GET http://localhost:8080/api/v1/tasks?page=0&size=0&sort=updatedAt,desc
Accept: application/json

# Очікуємо: 400 Bad Request + ProblemDetail з деталями щодо поля size

Очікуємо 400 Bad Request і Content-Type: application/problem+json (або еквівалентний формат Problem Details), а всередині — ваш code=INVALID_INPUT і деталі.

І третій приклад — помилка в sort. Тут особливо важливо не «проковтнути» друкарську помилку, бо інакше клієнт роками випадково сортуватиме «як вийшло».

### Непідтримуване поле сортування: немає у whitelist
GET http://localhost:8080/api/v1/tasks?page=0&size=20&sort=hackerField,asc
Accept: application/json

# Очікуємо: 400 Bad Request, бо sort порушує whitelist

Очікуємо 400 Bad Request, а в detail — зрозуміле повідомлення, що поле не підтримується.

Саме так і має працювати зріла колекція: набір параметрів може зростати, але розрізнення 200/400/404 залишається незмінним.

Щоб ще раз підкреслити різницю з 404, корисно порівняти це з detail endpointʼом:

### Не знайдено один ресурс
GET http://localhost:8080/api/v1/tasks/00000000-0000-0000-0000-000000000000
Accept: application/json

# Очікуємо: 404 Not Found, бо адресуємо конкретний ресурс за id

Ось це вже 404 Not Found, бо адресовано конкретний task resource. І це інший клас сценарію, ніж «список порожній».

Якщо тримати ці чотири запити поруч, у вас дуже швидко зафіксується правильна логіка: collection endpoint — завжди 200 за валідного запиту, навіть якщо «нічого»; 400 — лише за контрактні помилки запиту; 404 — за відсутності конкретного ресурсу.

5. Типові помилки під час семантики list

У цій темі помилки підступні тим, що вони рідко виглядають як «застосунок упав». Зазвичай усе працює, просто клієнт живе у світі недомовленостей. Тому краще впіймати ці моменти зараз, поки проєкт навчальний, а не тоді, коли у вас три клієнти, пʼять інтеграцій і один дуже нервовий продакт.

Помилка №1: повертати 404 Not Found для порожнього списку.
Це найпопулярніша плутанина. 404 означає, що за цим URI немає ресурсу. Але /api/v1/tasks — це ресурс колекції, він є завжди. Якщо задач немає, це не «ресурс не знайдено», а «колекція порожня». Клієнту від 404 стане тільки гірше, бо він увімкне аварійний сценарій замість порожнього стану.

Помилка №2: повертати 204 No Content для порожнього списку.
Іноді здається логічним: «нічого немає — отже, no content». Але у нас контракт списку — це PagedResponse, і навіть за порожніх items клієнту потрібні метадані (page, size, totalPages, sort). 204 за змістом забороняє тіло відповіді, а отже ви ламаєте власний контракт і змушуєте клієнта вгадувати.

Помилка №3: мовчки виправляти невалідні size/sort замість 400.
Якщо клієнт надіслав size=1000, а сервер «виправив» його на 100, клієнт і далі надсилатиме 1000. Те саме з sort: мовчазний fallback на значення за замовчуванням перетворює друкарську помилку на прихований режим роботи. Через місяць ніхто не памʼятає, чому результати «сортуються не так». Набагато чесніше й корисніше повернути 400 і змусити клієнта дотримуватися контракту.

Помилка №4: робити items = null замість порожнього списку.
Навіть якщо в Java вам простіше повернути null, для JSON-контракту це майже завжди помилка. Клієнт буде змушений писати зайві перевірки, а в деяких мовах і фреймворках це ще й призведе до падінь. Порожній список — це нормальна форма даних, і вона має бути стабільною.

Помилка №5: рахувати totalElements за розміром поточної сторінки.
Це ламає сенс пагінації. Метадані мають описувати всю вибірку, а не лише поточний зріз. Інакше клієнт не зможе зрозуміти, скільки всього сторінок, і не зможе побудувати нормальну навігацію. У нашому алгоритмі totalElements береться з повної (відсортованої) колекції, а items — зі slice.

1
Опитування
Пагінація API, рівень 22, лекція 4
Недоступний
Пагінація API
Списки, сторінки та сортування
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ