JavaRush /Курси /Java Server /Postman, README та packaging-артефакти

Postman, README та packaging-артефакти

Java Server
Рівень 26 , Лекція 3
Відкрита

1. Артефакти навколо коду

На цей момент API вже поводиться передбачувано не лише на happy-path: у нього є валідаційна межа, єдиний ErrorResponse і чесна різниця між 404, 405 та 409. Отже, настав час винести контракт назовні — у колекцію, README і, якщо хочете, у невеликий dist-архів.

Коли ви вперше дописуєте CRUD, дуже хочеться з полегшенням видихнути й сказати: «Ну все, готово, воно ж працює». Але «працює» — це стан у вашій голові, на вашому ноутбуці й у вашій поточній вкладці Postman. У бекенд-світі цінується інше: щоб результат можна було відтворити, перевірити й передати далі без телепатії.

Є просте правило: якщо проєкт не можна підняти й перевірити за інструкцією, то в нього немає зовнішньої форми. Він схожий на смачний суп… який існує лише в каструлі на вашій плиті. Начебто реальний, але щойно ви пішли — ніхто не знає, що саме ви туди поклали й скільки це варилося.

Давайте зафіксуємо, які три артефакти роблять наш ReadLater Starter завершеним:

Артефакт На яке запитання відповідає Чому це важливо саме зараз
Postman collection «Як перевірити API руками за 2–5 хвилин?» Це наш smoke-інструмент замість тестового фреймворку (пам’ятаєте межі курсу).
README.md «Як зібрати й запустити проєкт без магії IDE?» Завтра ви забудете деталі. README — це пам’ять проєкту, а не ваша.
Packaging (опційно) «Як віддати результат “одним файлом”, щоб він запускався?» Іноді зручно мати ZIP «для перевірки» без Gradle і без вихідних кодів.

Зверніть увагу на важливу думку: Postman collection і README — це не прикраси, а частина API-контракту та життєвого циклу застосунку. Spring потім автоматизує купу рутини, але він не замінить дисципліну: як проєкт запускається і перевіряється.

2. Postman collection як контракт API

Postman collection наприкінці курсу — це не «набір запитів, які я колись натискав». Це, по суті, маленька інструкція в машинному вигляді: які endpoints існують, які статуси вони повертають, як виглядають тіла запитів і відповідей, а також які негативні сценарії ми зобов’язані вміти розрізняти. В ідеалі будь-яка людина може імпортувати collection і за кілька хвилин зрозуміти, що API живий.

Організація collection та environments

Найсильніше Postman ламається, коли кожен запит живе своїм життям: URL захардкожені, імена «New Request (42)», а змінні існують лише у фантазії автора. Тому починаємо з дисципліни: одна collection, одна змінна baseUrl, зрозумілі імена запитів і мінімальна структура папок за сценаріями, а не за настроєм.

Зазвичай достатньо такої структури (у вигляді дерева, щоб було видно порядок):

ReadLater Starter — Local API
├─ Здоров’я
│  └─ GET /health
├─ Список читання
│  ├─ GET /api/v1/reading-list
│  ├─ GET /api/v1/reading-list/{id}
│  ├─ POST /api/v1/reading-list
│  ├─ PUT /api/v1/reading-list/{id}
│  ├─ PATCH /api/v1/reading-list/{id}/status
│  └─ DELETE /api/v1/reading-list/{id}
└─ Негативні
   ├─ 400 Validation error
   ├─ 404 Not found
   ├─ 405 Method not allowed (+Allow)
   ├─ 409 Conflict (externalId)
   └─ 500 Internal error (формат відповіді)

Ключовий трюк, який економить години життя: усюди пишемо URL через змінну середовища.

{{baseUrl}}/api/v1/reading-list

І заводимо кілька змінних (краще прямо в Environment, щоб не тримати їх у голові):

Змінна Приклад Для чого
baseUrl http://localhost:8080 Базова адреса сервера. Змінюється найчастіше.
itemId 1 ID створеного елемента, щоб потім не копіювати його вручну.
externalId OL12345M Зручно для сценарію 409 Conflict.

Якщо ви зараз подумали: «Та я й так пам’ятаю, що в мене localhost:8080», вітаю — це класична пастка. Завтра виявиться 8081. Післязавтра ви запустите два сервіси. А потім подивитеся на це через рік і спитаєте: «А чому тут у кожному запиті різні порти?»

Chained requests: зберігаємо id і використовуємо далі

Коли ви перевіряєте CRUD вручну, найнудніша частина — копіювати id з відповіді POST і вставляти його в GET/PUT/PATCH/DELETE. Postman уміє зробити це за вас: у тесті після POST ви зберігаєте id у змінну, і далі всі запити використовують {{itemId}}. Це рівно те саме зв’язування, тільки на рівні smoke-перевірки.

Спочатку зробимо зрозумілий body для створення. Зверніть увагу: JSON маленький і передбачуваний — це важливо для smoke-сценарію.

{
  "title": "Clean Code",
  "author": "Robert C. Martin",
  "status": "PLANNED",
  "externalId": "OL12345M",
  "comment": "Знайти паперове видання"
}

Тепер додамо мінімальний скрипт «Tests» у Postman для запиту POST /api/v1/reading-list (це JavaScript, але дуже маленький і дуже корисний):

// Перевіряємо, що створення справді відпрацювало як створення, а не як "200 OK на все підряд"
pm.test("201 Створено", () => {
  pm.response.to.have.status(201);
});

// Зберігаємо id створеного елемента, щоб не копіювати його вручну в наступні запити
const body = pm.response.json();
pm.environment.set("itemId", body.id);

Після цього ваш запит GET /api/v1/reading-list/{{itemId}} стає справді зручним. І це вже схоже на нормальний інженерний процес: ви не «клацаєте хаотично», а проганяєте сценарій.

До речі, для PATCH /status добре мати окремий невеликий body, щоб не змішувати повне оновлення й часткове:

{
  "status": "FINISHED"
}

А для PUT (full update) — повний набір полів, тому що це «заміна», а не «додамо те, що є»:

{
  "title": "Clean Code (2nd edition)",
  "author": "Robert C. Martin",
  "status": "IN_PROGRESS",
  "externalId": "OL12345M",
  "comment": "Перевидання, перевірити відгуки"
}

Negative-path сценарії: 400/404/405/409/500

Гарна фінальна collection — це та, де негативні сценарії не заховані «в голові». Ми не просто сподіваємося, що помилки будуть правильними; ми тримаємо в collection конкретні запити, які гарантовано приводять до очікуваного статусу та очікуваного ErrorResponse. Саме тут API стає контрактом, а не поведінкою «як вийшло».

Ось зручна таблиця негативних сценаріїв для нашого проєкту (і так, вона невелика — цього достатньо для курсу):

Сценарій Приклад запиту Очікуємо Що дивимось у відповіді
400 validation POST /api/v1/reading-list з порожнім title 400 errorCode=VALIDATION_ERROR, у details є причина
404 not found GET /api/v1/reading-list/999999 404 errorCode=NOT_FOUND і стабільне повідомлення
405 method not allowed PUT /api/v1/reading-list 405 Allow містить GET, POST (або ваш набір)
409 conflict два POST /api/v1/reading-list з однаковим externalId 409 errorCode=CONFLICT (і без стектрейсу)
500 internal «неочікувана» помилка 500 errorCode=INTERNAL_ERROR, повідомлення коротке, деталі порожні

Сценарій 400 найпростіше зробити так: надсилаємо валідний JSON-синтаксис, але ламаємо зміст. Наприклад, title — рядок із пробілів. Це важливий випадок: null і isBlank() — різні речі.

{
  "title": "   ",
  "author": "Robert C. Martin",
  "status": "PLANNED"
}

І очікуємо, що відповідь буде в єдиному форматі (приблизно так):

{
  "errorCode": "VALIDATION_ERROR",
  "message": "Запит не пройшов перевірку",
  "details": [
    "Поле title обов’язкове"
  ]
}

Сценарій 405 добрий тим, що він взагалі не має доходити до parsing/validation. Ми перевіряємо його на рівні маршрутизації: шлях відомий, метод — ні. І, як дорослий API, ми повертаємо Allow. У Postman можна навіть зробити маленьку перевірку, що header справді приїхав:

// 405 — це про роутинг, тому окремою перевіркою фіксуємо і статус, і наявність Allow
pm.test("405 + заголовок Allow", () => {
  pm.response.to.have.status(405);
  pm.expect(pm.response.headers.has("Allow")).to.be.true;
});

Про 500 скажу чесно: у маленькому навчальному проєкті ви не завжди можете «примусово відтворити» внутрішню помилку так само стабільно, як 404 або 409. Але сенс сценарію не в тому, щоб милуватися 500, а в тому, щоб якщо він станеться (а він станеться — хоча б через майбутній рефакторинг), клієнт побачив нормальний JSON, а не «Oops» і не стектрейс на 200 рядків. Тому в collection має сенс тримати запит «500 internal error (format check)» і час від часу переконуватися, що формат помилки залишається коректним.

3. README: інструкція з запуску та перевірки

README — це не «пост у стилі: я написав проєкт, усім дякую». У нашому курсі README — це інструкція для запуску та перевірки. Уявіть, що ваш проєкт завтра має перевірити інший студент, або ви самі через тиждень у стані «я нічого не пам’ятаю, але треба швидко підняти». README має відповідати на запитання без діалогу з автором, інакше це не README, а загадка.

Мінімальний шаблон README

Шаблон README краще тримати коротким, але конкретним. У ньому важливі команди через Gradle Wrapper, режими запуску та мінімальні підказки щодо конфігурації. Не треба перетворювати README на роман. Але й «Run it somehow» теж не підходить — це надто чесно навіть за мірками програмістів.

Приклад мінімального блоку «Запуск» (зверніть увагу: команди короткі та перевіряються):

## Запуск

Збирання:
./gradlew clean build

Запуск server-mode:
./gradlew run --args="server"

Далі має сенс додати «швидкий smoke» прямо текстом, щоб людина не шукала очима:

## Швидка перевірка

1) GET {{baseUrl}}/health -> 200
2) POST /api/v1/reading-list -> 201 + Location
3) GET /api/v1/reading-list -> 200 + count

Так, тут є нумерація, але вона виконує роль інструкції. У README без неї виходить вода, а наша мета — відтворюваність.

Окремо важливо зафіксувати, де лежать артефакти:

## Артефакти

- Postman collection: postman/readlater.postman_collection.json
- Конфіг: src/main/resources/application.properties

І ще один практичний фрагмент — приклад конфігурації. Навіть якщо у вас за замовчуванням усе «і так працює», через місяць ви забудете ключі.

# На якій адресі/порті піднімається сервер у server-mode
server.host=localhost
server.port=8080

# У якому режимі працюємо з каталогом: mock / real (або ваш варіант)
catalog.api.mode=mock
catalog.api.base-url=https://example-catalog

Smoke-checklist: «пʼять хвилин — і сервіс живий»

Перевірка проєкту має бути короткою й однаковою кожного разу. Тому добре працює концепція smoke-checklist: невеликий порядок кроків, який ви можете виконати навіть у втомленому стані. У нашому курсі це особливо важливо, тому що ми свідомо не будуємо окремий тестовий контур — замість нього в нас Postman і зрозумілі команди запуску.

Найзручніше тримати checklist або прямо в README, або окремим блоком поруч, але в межах курсу README достатньо. Наприклад, можна оформити це таблицею:

Крок Дія Очікуваний результат
1 ./gradlew run --args="server" У логах видно host/port, сервер піднявся
2 GET /health 200 OK і JSON зі статусом UP
3 POST /api/v1/reading-list 201 Created, у відповіді є id
4 GET /api/v1/reading-list/{{id}} 200 OK і коректний об’єкт
5 PUT /api/v1/reading-list/{{id}} або PATCH /api/v1/reading-list/{{id}}/status 200 OK, зміни видно в наступному GET
6 DELETE /api/v1/reading-list/{{id}} 204 No Content, після цього GET дає 404

Це виглядає як формальність, доки ви не спробуєте перевірити проєкт через тиждень, коли в голові вже Spring, SQL і думки про сенс життя. Тоді така таблиця — чиста психотерапія.

4. Мінімум файлів у репозиторії

Іноді новачки думають, що репозиторій — це лише src/ і build.gradle.kts. Усе інше сприймається як зайва бюрократія. Але backend-проєкт — це артефакт, який живе не тільки в коді: у нього є конфігурація, логи, sample-дані, smoke-інструменти. Якщо ці речі розкидані де завгодно, проєкт важко перевіряти й важко підтримувати навіть самому собі.

Для ReadLater Starter достатньо такого мінімального складу (приблизно так це виглядає «доросло», але без переускладнення):

.
├─ build.gradle.kts
├─ settings.gradle.kts
├─ gradlew / gradlew.bat
├─ README.md
├─ postman/
│  └─ readlater.postman_collection.json
├─ samples/ (або src/main/resources/mock/)
│  ├─ catalog-search.json
│  └─ catalog-details.json
└─ src/
   └─ main/
      ├─ java/...
      └─ resources/
         ├─ application.properties
         └─ logback.xml

Тут важливий сенс: Postman-артефакт лежить поруч із кодом, тому що це частина проєкту; конфіг лежить у ресурсах, тому що це нормальна точка для зовнішньої поведінки; samples лежать там, де їх можна знайти й використати в mock-режимі. А README лежить у корені, тому що так влаштоване життя.

5. Packaging: ZIP без Gradle

Packaging наприкінці курсу — штука опційна. Це не обов’язковий DevOps, не деплой і не контейнеризація, а лише зручний спосіб зібрати проєкт у переносний набір файлів. Іноді це допомагає викладачеві перевіряти роботи, іноді — вам самим переносити проєкт між машинами, а іноді — просто відчути, що «ось він, артефакт».

Звичайний jar і runtime classpath

Коли ви робите ./gradlew jar, Gradle збирає звичайний jar. У ньому є ваш код, але немає залежностей (Jackson, SLF4J, Logback). І це нормально: ми в цьому курсі не підключаємо Shadow plugin і не робимо fat-jar, тому що це окрема тема й окремі нюанси.

Тому якщо ви спробуєте «чесно» запустити лише jar, то рано чи пізно побачите знайоме сумне:

NoClassDefFoundError: com/fasterxml/jackson/...

Рішення на рівні простого дистрибутива таке: у ZIP кладемо jar і кладемо поруч усі runtime-залежності, а запуск робимо через classpath wildcard. Команда виходить зрозуміла:

java -cp "libs/*" com.example.readlater.app.ReadLaterApplication server

І це виглядає майже як мініінсталятор: є папка libs/, є команда запуску, є README і Postman.

Gradle Zip task: packageDist

Gradle уміє пакувати файли в ZIP без додаткових плагінів. Ми просто реєструємо задачу packageDist, кладемо в архів jar, runtime classpath, README і папку postman/. Це невелика інфраструктура, але вона дуже прозора: ви бачите, що саме пакуєте, і немає відчуття магії.

Приклад задачі в build.gradle.kts (усе навмисно коротко):

import org.gradle.api.tasks.bundling.Zip

tasks.register<Zip>("packageDist") {
    // Кладемо зібраний jar у папку libs всередині архіву
    from(tasks.jar) { into("libs") }

    // Кладемо runtime-залежності поруч, щоб запускати через -cp "libs/*"
    from(configurations.runtimeClasspath) { into("libs") }

    // Додаємо інструкцію із запуску поруч — інакше ZIP перетворюється на "вгадай як"
    from("README.md")

    // Додаємо smoke-інструмент (Postman collection) як частину артефакту
    from("postman") { into("postman") }

    // Явно задаємо, куди складати ZIP і як його назвати (щоб шлях був передбачуваним)
    destinationDirectory.set(layout.buildDirectory.dir("dist"))
    archiveFileName.set("readlater-starter.zip")
}

Після цього ви можете виконати:

./gradlew packageDist

І отримати build/dist/readlater-starter.zip. Усередині буде достатньо файлів, щоб «взяти й спробувати». І це важливо: упаковка має бути невеликою, а не перетворюватися на копію репозиторію з усім сміттям.

Мініскрипти запуску: run.sh та run.bat

Якщо вже робите ZIP, людині буде приємніше запускати не довгу команду, а короткий скрипт. Але ми не перетворюємо це на окремий інженерний пласт: скрипт — на 5–10 рядків, без розумних перевірок на все підряд.

Приклад run.sh (для Linux/macOS):

#!/usr/bin/env bash
set -e

# Перший аргумент — режим, за замовчуванням "server" (щоб запуск без параметрів працював)
MODE="${1:-server}"

# Запускаємо застосунок із classpath на libs/* (jar + залежності лежать поруч)
java -cp "libs/*" com.example.readlater.app.ReadLaterApplication "$MODE"

І приклад run.bat (для Windows, теж максимально просто):

@echo off

rem Перший аргумент — режим, якщо не задано, використовуємо server
set MODE=%1
if "%MODE%"=="" set MODE=server

rem Запуск через classpath на libs/* (jar + залежності в одній папці)
java -cp "libs/*" com.example.readlater.app.ReadLaterApplication %MODE%

В ідеалі ви кладете їх в архів разом із README. І знову тримаємо межу: це не справжній інсталятор, а маленька зручна кнопка «запусти».

6. Перевірка завершеності проєкту

Дуже корисна перевірка — «тест на завтрашнього себе». Уявіть, що ви закрили IDE, видалили всі run-конфіги, забули, який порт був учора, і раптом вам потрібно показати проєкт. Якщо в цей момент ви можете відкрити README, запустити команди і прогнати Postman collection — значить, проєкт справді завершений як навчальний артефакт.

Можна перевірити це буквально за невеликою таблицею, без героїзму:

Перевірка Що робимо Що має бути видно
CLI build ./gradlew clean build Збирання зелене, jar збирається
Server run ./gradlew run --args="server" Сервер стартує на порту з конфігурації
Health GET /health 200 і JSON UP
CRUD smoke колекція Postman happy-path проходить без ручного копіпасту id
Negative папка Negative в collection 400/404/405/409 повертають ErrorResponse

Якщо все це виконується, далі вже можна спокійно закривати курс. Бо ви не просто «написали API», а зробили результат, який можна показати, перевірити й розвивати далі.

7. Типові помилки

Помилка №1: README «лише для IDE».
Коли запуск описано через IntelliJ («натисніть зелену кнопку»), проєкт перестає бути відтворюваним поза вашою середою. Будь-який інший розробник упирається в налаштування IDE замість запуску застосунку. Базовий сценарій має будуватися навколо ./gradlew — це і є контракт запуску. IDE — лише зручний інтерфейс, а не основний спосіб роботи.

Помилка №2: Postman collection лише з happy-path.
Наявність GET і POST — це ще не контракт. Якщо в колекції немає негативних кейсів (400, 404, 405, 409) і перевірки Allow для 405, ви не тестуєте поведінку системи в реальних умовах. Зовнішній клієнт помиляється — і саме в цей момент проявляється якість API. Негативні сценарії мають бути такими ж «першокласними», як і успішні.

Помилка №3: захардкожений localhost:8080 у кожному запиті.
Поки все працює на одному порту, це здається нормою. Але будь-яка зміна перетворюється на ручне виправлення десятків запитів. Один baseUrl у environment повністю розв’язує цю проблему й робить колекцію переносною між машинами, стендами та середовищами.

Помилка №4: неправильні очікування від jar під час packaging.
Звичайний jar, зібраний Gradle, не містить залежностей — і це коректно. Помилка виникає тоді, коли його намагаються запускати «як є». Якщо ви робите ZIP-дистрибутив, у вас два шляхи: або зібрати fat-jar (якщо це дозволено), або покласти всі runtime-залежності в libs/ і запускати через classpath. Усе інше — спроба обійти реальність збирання.

Помилка №5: архів «з усім підряд».
Коли в ZIP потрапляють .idea/, build/, тимчасові файли та випадкові артефакти, ви ускладнюєте перевірку й збільшуєте шум. Packaging — це про мінімальний, чистий набір: jar, залежності, README, Postman. Усе, що не потрібно для запуску й перевірки, має залишатися в репозиторії, а не в доставці.

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