JavaRush /Курси /Spring REST & MVC /Перше знайомство зі Spring REST & MVC

Перше знайомство зі Spring REST & MVC

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

1. Швидко піднятий ендпоінт — це ще не контракт

Давайте почнемо з дуже типової картини. Розробник уже вміє запускати застосунок Spring Boot і пише щось на кшталт цього:

@RestController // Кажемо Spring: це REST-контролер, відповіді серіалізуватимуться (зазвичай у JSON)
@RequestMapping("/api/v1/tasks") // Базовий префікс для всіх маршрутів цього контролера
class TaskController {

    private final TaskService taskService; // Залежність від сервісу: бізнес-логіка та доступ до даних живуть не в контролері

    TaskController(TaskService taskService) { // Конструкторна інʼєкція: залежність обов’язково має бути передана
        this.taskService = taskService; // Зберігаємо сервіс, щоб використовувати в обробниках запитів
    }

    @GetMapping("/{taskId}") // GET /api/v1/tasks/{taskId} — отримання ресурсу за ідентифікатором у шляху
    Task getById(@PathVariable String taskId) { // taskId береться з URL (наприклад /api/v1/tasks/t-42)
        return taskService.findById(taskId); // Повертаємо внутрішню модель напряму (це і є потенційна проблема контракту)
    }
}

З погляду Java та Spring Boot усе виглядає цілком нормально ⚙️. Є контролер, маршрут, виклик сервісу та результат. Застосунок стартує, ендпоінт відповідає, Postman показує JSON. У цей момент легко подумати: «Ну все, API готовий». І саме тут починається тонка, але дуже важлива помилка.

Проблема не в тому, що код «поганий». Проблема в тому, що за цим фрагментом узагалі не видно, який у вас зовнішній договір із клієнтом. Що буде, якщо taskId не існує? Що ви повернете, якщо внутрішній Task містить поля, які клієнту бачити не слід? Яким буде формат помилки? Чи отримає клієнт 404, 500 або просто рядок "задачу не знайдено"? Чи можна вважати назви полів стабільними?

Тобто робочий ендпоінт і зрілий API — це не одне й те саме. Робочий ендпоінт відповідає на запитання: «Чи може застосунок технічно повернути щось по HTTP?» Зрілий API відповідає на запитання: «Чи може клієнт безпечно й передбачувано залежати від цієї поведінки?» І це вже зовсім інший рівень розмови.

Корисно одразу зафіксувати одну тверезу річ. Spring Boot дуже добре допомагає швидко зібрати web-застосунок 🔮. Але продуктові та контрактні рішення він за вас не приймає. Він не вирішує, які поля ви обіцяєте клієнту, яким буде статус за різних сценаріїв, як ви оформлюєте помилки, де проходять межі між внутрішньою моделлю та зовнішнім JSON. Фреймворк допомагає побудувати дорогу, але не обирає маршрут.

2. Що клієнт насправді отримує від вашого API

Щоб відчути різницю не абстрактно, а на практиці, достатньо подивитися не на метод контролера, а на дві відповіді, які справді побачить клієнт. Візьмемо дуже простий сценарій: клієнт запитує задачу за ідентифікатором.

Якщо задача знайдена, він може отримати ось таке:

# Статус успіху: ресурс знайдено і повернуто
HTTP/1.1 200 OK
# Сервер явно повідомляє клієнту, що далі буде JSON
Content-Type: application/json

# Тіло відповіді: публічні поля, на які клієнт покладатиметься
{
  "id": "t-42",
  "title": "Виправити формат помилки валідації",
  "status": "IN_PROGRESS"
}

Якщо задачу не знайдено, зрілий API має вміти відповідати не «як вийде», а структуровано. Наприклад, так:

# Статус помилки: ресурсу з таким ідентифікатором немає
HTTP/1.1 404 Not Found
# Типовий формат помилок за RFC 7807 (Problem Details)
Content-Type: application/problem+json

# Тіло помилки: структурований опис, щоб клієнт міг зрозуміти причину й реагувати передбачувано
{
  "type": "https://example.dev/problems/task-not-found",
  "title": "Задачу не знайдено",
  "status": 404,
  "detail": "Задачу з id t-42 не знайдено"
}

Подивіться, що тут важливо 🍄. Клієнт не знає, чи був у вас TaskService, Map, JPA, if, switch або сотня рядків рефакторингу. Для нього важливо лише таке: за успіху є 200 OK і зрозумілий JSON; за відсутності ресурсу — 404 Not Found і зрозумілий опис помилки. На цьому він будує свою поведінку. Фронтенд відображає екран. Мобільний застосунок показує повідомлення. Інший сервіс розуміє, чи потрібно повторювати запит. Автотест звіряє статус і форму JSON.

І ось перша ключова думка. API — це не ваш код. API — це спостережувана поведінка на зовнішній межі продукту. Клієнт інтегрується не з класом TaskController, а з набором обіцянок: який шлях існує, який формат відповіді стабільний, які помилки можливі, що означають статуси.

Якщо зовсім коротко, формула така: клієнт інтегрується з поведінкою, а не з реалізацією. Це проста думка, але в неї дуже дорогі наслідки. Щойно ви починаєте бачити API саме так, одразу стає зрозуміло, чому не можна ставитися до помилок як до сміття, чому поля JSON не можна бездумно змінювати, чому випадковий 500 руйнує довіру до контракту, а не просто «робить лог червоним».

3. Перевірка якості: один успіх і одна помилка

На старті не потрібно знати всі тонкощі REST, HTTP-семантики та внутрішніх механізмів MVC. Уже одна успішна відповідь і одна відповідь з помилкою дуже швидко показують, наскільки зрілим виглядає API 📉📈. Це майже як рентген: двох знімків досить, щоб побачити перелом.

Порівняйте два підходи:

Запитання клієнта Просто робочий ендпоінт Зрілий API-контракт
Що я отримаю за успіху? «Якийсь JSON» Зрозумілий статус і передбачувану форму відповіді
Що я отримаю в разі помилки? Випадковий рядок, помилку фреймворка або 500 Змістовний статус і єдине тіло помилки
Які поля тут стабільні? Незрозуміло, залежить від внутрішньої моделі Зрозуміло, бо контракт відокремлено від реалізації
Що саме обіцяє сервер? «Метод начебто працює» Конкретну поведінку в успішному та негативному сценаріях

Ось де починається професійний рівень. Не там, де ви знаєте більше анотацій, а там, де вмієте утримувати обидві сторони контракту: успішну й помилкову. Поки розробник думає лише про вдалий сценарій, він пише ендпоінт для демо. Коли він однаково серйозно проєктує і успіх, і помилку, він починає писати API, з яким можна жити в реальній системі.

Це особливо помітно в типовій проблемі новачків 💥. Застосунок часто «працює», поки всі запити ідеальні. Але щойно приходять некоректні вхідні дані, неіснуючий ID, кривий enum, відсутнє тіло, неочікуване поле або неправильний формат, поведінка стає випадковою. Десь 400, десь 500, десь довге exception message, десь HTML-сторінка помилки, десь просто порожня відповідь. Клієнт починає вгадувати. А вгадування — найгірший тип інтеграції.

Поки що нам не потрібно обговорювати, як саме будується цей error contract усередині Spring MVC. Достатньо побачити, що він узагалі має існувати. Цей курс і буде крок за кроком будувати саме таку дисципліну: щоб кожен ендпоінт був не «методом, який щось повертає», а частиною зібраного зовнішнього контракту.

4. Іноді Spring Boot допомагає, а іноді мовчить

Дуже важливо зараз не скотитися в хибну війну «Spring Boot проти API design». Жодної війни тут немає 🙂. Spring Boot робить величезну корисну роботу: швидко підіймає застосунок, дає передбачуваний старт, допомагає збирати залежності, піднімає web-шар і дозволяє не тонути в ритуалах. Без нього цей курс узагалі був би методично важчим.

Але в Boot є природна межа відповідальності. Він допомагає вам швидко отримати робочий web-механізм, а не автоматично зрілий зовнішній контракт. Це дві різні задачі.

Ось різниця в одному короткому порівнянні:

Що Spring Boot сильно спрощує Що він не проєктує за вас
Старт застосунку та web-шару Семантику URI та операцій
Wiring, конфігурацію, базове середовище виконання Форму успішної відповіді
Технічну можливість повернути JSON Форму відповіді з помилкою
Підключення стандартних бібліотек Межу між внутрішньою моделлю та DTO
Швидкий шлях до першого ендпоінта Правила еволюції публічного контракту

Це не вада Boot. Це чесна межа його ролі. Фреймворк не повинен магією вирішувати продуктові та контрактні запитання. Інакше ми отримували б не платформу, а генератор випадкових архітектурних рішень. Тому окремий курс після базового Spring Boot тут абсолютно логічний: спочатку ви вчитеся підіймати застосунок, потім — робити його зовнішню межу зрілою.

На практиці це означає просту річ. Якщо ви вже вмієте писати @RestController, ви прийшли на цей курс не надто пізно. Навпаки, ви прийшли в правильний момент. Тепер у вас є технічна опора, і можна нарешті говорити не про те, як змусити ендпоінт відповідати, а про те, як саме він має відповідати правильно.

5. П’ять клієнтських запитань до будь-якого ендпоінта

Щоб сьогоднішня розмова не залишилася красивою теорією, винесемо з неї одну корисну лінзу. Через неї зручно дивитися на будь-який ендпоінт — і навчальний, і робочий. Її зручно тримати поруч із .http запитом, із чернеткою методу контролера або просто в голові під час code review.

Запитання Що ви перевіряєте очима клієнта
1. Що це за ресурс або сценарій? За method + path зрозуміло, що саме клієнт запитує або змінює
2. Як виглядає успішна відповідь? Є чіткий статус, зрозуміла форма JSON і передбачувані поля
3. Як виглядає помилка? У помилки є змістовний статус і структуроване тіло відповіді
4. Чим керує клієнт, а чим сервер? Видно, які дані приходять ззовні, а які призначає сам застосунок
5. Що тут має залишитися стабільним завтра? Зрозуміло, які імена полів, статуси та правила не можна змінювати випадково

У цієї таблиці є приємна властивість. Вона працює до коду, під час коду і після коду. До коду вона допомагає не писати методи контролера навмання. Під час коду не дає злити назовні внутрішню модель або забути про негативні сценарії. Після коду допомагає швидко перевірити, чи не перетворився ендпоінт на «воно в мене працює, але я не знаю, що я обіцяв клієнту».

6. Застосуємо запитання до задачі Task Tracker API 🔎

Тепер давайте прикладемо цю лінзу до одного майбутнього ендпоінта з нашого проєкту: GET /api/v1/tasks/{taskId}. Нам зараз не потрібні ні повна карта API, ні вся архітектура проєкту. Достатньо одного зрізу.

Запитання лінзи Коротка відповідь для GET /api/v1/tasks/{taskId}
Що це за сценарій? Клієнт хоче отримати одну задачу за її ідентифікатором
Як виглядає успіх? 200 OK і JSON із публічними полями задачі
Як виглядає помилка? 404 Not Found і структурований error response
Чим керує клієнт? Лише taskId у шляху; сервер сам визначає, як і звідки брати дані
Що має бути стабільним? Сам шлях до ресурсу, форма публічного JSON і модель помилок

Найцікавіше тут — те, чого ми не обговорювали 🙃. Ми не говорили, де лежать дані. Не обговорювали JPA. Не чіпали security. Не лізли в деталі Spring MVC internals. І все одно вже провели цілком професійну розмову про API. Для старту курсу це дуже важливе відчуття.

7. Типові помилки першого погляду 🚧

Помилка №1: дивитися на ендпоінт лише крізь Java-метод.
Це найпоширеніша стартова пастка. Розробник бачить @GetMapping, сигнатуру методу і вважає, що картина вичерпна. Але клієнт не має доступу до вашої сигнатури. Для нього існують лише status code, headers і body. Якщо не переключитися на цей зовнішній погляд, далі весь API проєктуватиметься «для автора коду», а не для споживача.

Помилка №2: вважати успішний JSON достатньою ознакою якості.
Дуже спокусливо думати: «Ну, адже відповідь є, отже все нормально». Ні. API стає зрілим лише тоді, коли передбачувані і успіх, і помилка. Якщо у вас охайний лише успішний сценарій, а негативний — випадковий, контракт усе одно слабкий.

Помилка №3: думати, що Spring Boot сам «додумає» контракт за вас.
Фреймворк не обирає за вас, що обіцяти клієнту. Він допомагає швидко побудувати web-механізм, але не замінює інженерне рішення про те, де проходить межа контракту, які поля є публічними та як виглядає модель помилок.

Помилка №4: сприймати error response як другорядну дрібницю.
Для клієнта помилка — не сміттєвий сценарій, а частина нормального життя API. Якщо вона не спроєктована, клієнтський код починає працювати на здогадках. А здогадки завжди дорожчі, ніж один раз продуманий договір.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ