Що не так з загальними практиками REST API

Мене звати Юрій Івон, я співпрацюю з компанією EPAM як Senior Solution Architect. Цього разу хотів би поділитися своїми міркуваннями щодо доцільності деяких практик проєктування REST API.

REST-сервіси, їхні моделі зрілості та відповідні «найкращі практики» існують вже досить давно. Безліч статей пояснюють, як створювати RESTful API, і, ймовірно, сотні тисяч годин були витрачені на обговорення правильного використання HTTP-дієслів і принципів іменування ендпоінтів. Давайте знову переглянемо цей досвід і спробуємо відповісти на кілька питань — чи все це було того варте, а якщо ні, то як ми можемо зробити краще?

Концепції REST були представлені Роєм Філдінгом у його докторській дисертації як пропозиція оптимального архітектурного стилю для розподілених систем. Вона є досить загальною і не містить детальних рекомендацій щодо проєктування API, але окреслює кілька ідей, які згодом стали основою практичного підходу.

Варто зазначити, що серед основних архітектурних обмежень, визначених Філдінгом, лише три безпосередньо стосуються проєктування API-контракту: відсутність стану (statelessness), кешованість (cacheability) та уніфікований інтерфейс (uniform interface). Відсутність стану не повинна викликати жодних питань, оскільки вже багато років є загальноприйнятою практикою. Однак певні аспекти кешування та уніфікованого інтерфейсу потребують повторного перегляду.

Ресурси

Можливо, найменш інтуїтивною ідеєю, запропонованою Філдінгом, є те, що все в API має бути представлене як ресурс, який ідентифікується унікальним URI. Коли ви маєте справу з тривіальними CRUD-операціями над окремими об’єктами домену та їх колекціями, все виглядає гарно й просто. Але ситуація починає ускладнюватися, коли потрібно спроєктувати процесно-орієнтований API.

Припустімо, у нас є система керування завданнями, де кожне завдання можна запускати, зупиняти, призупиняти та відновлювати. Найочевидніші HTTP-ендпоїнти для керування виконанням вже створеного завдання виглядали б так:

  • /api/v1/jobs/{id}/start
  • /api/v1/jobs/{id}/stop
  • /api/v1/jobs/{id}/pause

Однак вони не є ресурсно-орієнтованими, як передбачає концепція REST. Щоб виразити ті самі можливості ресурсно-орієнтованим способом, ми могли б мати кілька альтернатив:

  • /api/v1/jobs/{id}/status — очікує PUT з тілом запиту, яке вказує новий стан для завдання.
  • /api/v1/jobs/{id} — очікує PATCH з тілом запиту, яке може перезаписати будь-яку властивість завдання, в тому числі і його стан. Логіка на бекенді оброблятиме оновлення поля стану спеціальним чином для керування життєвим циклом завдання.

Очевидно, що для цього випадку існує варіант мати ресурсно-орієнтований API, але чи дає він щось, окрім концептуальної чистоти REST? Я вже явно бачу кілька недоліків:

  • Призначення ендпоїнту стає менш очевидним.
  • Бекенд змушений приховувати багато умовної логіки за однією операцією поверх простого оновлення об’єкта («якщо статус змінився — зробити щось особливе залежно від його значення»).

Але найбільше мене турбує концептуальна невідповідність між ресурсно-орієнтованим підходом і реальними потребами взаємодії між застосунками. Незалежно від походження цієї ідеї чи того, як кінцеві користувачі взаємодіють із вебресурсами через браузер, застосунки взаємодіють між собою переважно процедурним способом.

Коли потрібно прочитати замовлення — викликають метод getOrder або будь-який інший подібний за назвою метод; коли потрібно запустити чи призупинити завдання — викликають методи startJob і pauseJob відповідно (або методи start і pause об’єкта завдання). І незалежно від того, як виглядають REST-ендпоїнти, практично в будь-якій мові програмування взаємодія із сервісом робиться через дієслівну семантику. То який сенс вносити таку невідповідність?

A screenshot of a computerAI-generated content may be incorrect.

У наведеному вище прикладі перший варіант є простим і логічним, і клієнтський код може бути прозоро згенерований із визначення API.

Другий варіант вимагає додаткового коду як на клієнті, так і на сервері. Різні переходи станів завдання (або будь-якого іншого об’єкта, що має переходи станів) зазвичай передбачають різні набори дій, які природніше всього реалізовувати у вигляді окремих методів. Отже, другий варіант вимагає від нас:

  • В коді клієнту «транслювати» виклик методу («pause») у виклик REST API для оновлення статусу. Не існує автоматизованого способу згенерувати клієнтську бібліотеку з API-специфікації, яка б підтримувала такі семантичні переходи, тому обгортку навколо REST-виклику доведеться писати вручну.
  • «Транслювати» оновлення статусу на бекенді у відповідний виклик методу бізнес-логіки.

Виявляється, що RESTful-підхід виглядає громіздким і вимагає більше зусиль. Чому ж Рой Філдінг наполягав на дотриманні саме цього підходу?

Схоже, ідея розглядати все як ресурс тісно пов’язана з концепцією HATEOAS, у якій від клієнтів очікується, що вони будуть «пересуватись» застосунком за допомогою посилань, наданих у відповідях сервера. Такі гіпотетичні клієнти відображали б інтерфейс користувача суто на основі отриманої інформації, яка включає поля даних і навігаційні посилання. Ці посилання давали б користувачам змогу переглядати пов’язані об’єкти та виконувати дії. Представлення дій у вигляді ресурсів спрощує модель, з якою мали б працювати такі клієнти. Це навіть робить її «академічно цілісною», адже не потрібні нові «дієслова» крім тих, що вже існують в HTTP, а всі інші об’єкти представлені у вигляді іменників.

Полегшення розробки та підтримки клієнтів для кінцевих користувачів є цілком слушною метою, але сама ідея викликає чимало занепокоєнь:

  • Створити зручний для користувача інтерфейс, в якому набір і розташування елементів формується лише на основі даних, майже неможливо. Поля даних та команди мають бути розташовані найзручнішим для кінцевого користувача чином, що потребує роботи з UX-дизайну і не може бути точно визначено лише з даних. Є випадки, коли UX не має вирішального значення і можна зробити припущення щодо нього — наприклад, у випадку типових адміністративних інтерфейсів, — але це радше виняток, ніж правило.
  • Значна частина сучасних API розробляється для взаємодії між застосунками, а не лише для забезпечення інтерфейсів користувача: платформа інтернет-магазину викликає пошуковий рушій, скрипт автоматизації — сервіс сповіщень, сервіс обробки документів — GenAI-сервіс для пошуку важливих атрибутів тощо. У цьому контексті HATEOAS не має жодного сенсу. Система, що викликає, чітко «знає», що саме їй потрібно, і потребує простого, детермінованого способу викликати конкретний метод без необхідності проходити крізь дерево об’єктів у пошуках відповідного ресурсу.

Пройдімо ще раз цей ланцюжок міркувань. Вимога, щоб клієнт взаємодіяв з сервером виключно через ресурси, потрібна для ефективної підтримки HATEOAS, яка необхідна, аби будувати «тонкі» клієнти для користувача, що не містять доменних знань. Однак у більшості випадків практично неможливо створити такий клієнт без «хардкоду» доменних знань, а значна частина API взагалі не призначена для клієнтських застосунків. Виглядає так, що вимога взаємодіяти лише через ресурси не має практичної користі.

Дієслова

Існує кілька загальновідомих рекомендацій щодо того, які HTTP-дієслова слід використовувати в кожній ситуації, і їх доволі часто дотримуються як догми, без повного розуміння практичних компромісів:

  • Використовуйте GET для всіх операцій читання
  • Використовуйте POST для створення нового ресурсу
  • Використовуйте PUT для оновлення існуючого ресурсу
  • ...

Якщо відкинути семантику, то на практиці важливі лише дві властивості, які можуть бути різними у різних дієслів — кешування та ідемпотентність:

  • Зазвичай лише відповіді на запити GET і HEAD можуть бути закешовані за допомогою стандартних механізмів протоколу HTTP.
  • Усі стандартні дієслова HTTP, окрім POST і PATCH, вважаються ідемпотентними згідно зі специфікацією HTTP (RFC 7231). Хоча PATCH може бути реалізований як ідемпотентний, специфікація цього не гарантує. Ідемпотентні операції можна безпечно повторювати, тому різні HTTP-сервери та проксі (наприклад, Nginx) можуть мати вбудовані правила для повтору проблемних запитів лише для ідемпотентних дієслів, уникаючи таким чином небажаних побічних ефектів.

Уявімо, що ми реалізували API на основі HTTP, де всі методи викликаються через дієслово POST незалежно від семантики методу. Нам потрібно зрозуміти, що ми втрачаємо з таким підходом.

Перша проблема полягає в тому, що методи «тільки для читання», такі як отримання списку об’єктів, не будуть кешуватися за замовчуванням в браузерах і на проксі-серверах. Але чи є це великою проблемою? Я можу навести декілька причин, чому це неважливо в більшості випадків.

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

Динамічна персоналізація ускладнює кешування відповідей за допомогою стандартних засобів HTTP-протоколу. Багато сучасних GET-методів повертають дані, специфічні для користувача. Це вимагає використання заголовка «Vary», щоб розділяти записи кешу для різних користувачів. Хоча теоретично можуть існувати ситуації, коли цей заголовок не забезпечує повного розділення, мене ще більше непокоїть потенційне розкриття даних, коли користувач випадково отримує інформацію, призначену для іншого.

Заголовки відповіді є дуже непрямим механізмом керування поведінкою системи, тому ймовірність випадкового видалення необхідних заголовків або надання неправильного значення вища, ніж у разі, якщо логіка кешування була б реалізована безпосередньо в коді клієнта. Випадкове видалення заголовка ‘Vary’ може призвести до серйозного розкриття даних.

Стандартне кешування не контролюється централізовано — якщо з якоїсь причини в кеш браузера потрапили неправильні дані, може бути неможливо видалити їх для всіх користувачів одночасно.

Сучасні рішення мають кілька інших, більш гнучких рівнів кешування, що робить HTTP-кеш непотрібним: на бекенді (зазвичай розподілене), у CDN із розширеними можливостями поза стандартом HTTP, а також у клієнтських застосунках.

Ще однією потенційною проблемою може бути неможливість налаштувати автоматичні повторні спроби (retries) для методів API на рівні вебсервера або проксі. Проте я взагалі не вважаю це проблемою. Я б надав перевагу реалізації повторних спроб у клієнтських застосунках, щоб мати кращий контроль та прозорість.

Отже, виявляється, що немає суттєвих практичних переваг у суворому дотриманні початкових конвенцій. Пригадую пару ситуацій, коли команда розробників мала створити API-метод, який повертає дані на основі критеріїв потенційно необмеженої складності. В обох випадках перша версія методу використовувала дієслово GET, оскільки «рекомендовано реалізовувати операції читання за допомогою GET».

Однак таке рішення мало щонайменше два суттєві недоліки:

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

Використання POST вирішило б обидві проблеми завдяки передачі параметрів у тілі запиту, але автор методу вперто стверджував, що це «не буде RESTful». На жаль, це типовий приклад карго-культу в інженерії програмного забезпечення — коли реальну проблему ігнорують заради дотримання теоретичної догми. Такі ситуації слід вирішувати, а не замовчувати. У моїх прикладах проблему зрештою вдалося виправити після чіткого, детального та спокійного пояснення недоліків та переваг.

Призначати дієслова методам API відповідно до їхньої семантики — цілком логічно. Було б дивно бачити метод «отримати об’єкт», реалізований через дієслово DELETE, а «видалити об’єкт» — через GET. Однак є ситуації, коли краще використати POST замість GET, і це цілком нормально.

Чого не вистачає?

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

Специфікації API

Хоча специфікація OpenAPI (раніше Swagger) існує вже давно, я й досі бачу проєкти, які не використовують її належним чином. Багато розробників знають, що документацію для API та інтерактивний playground можна згенерувати автоматично з коду сервера, але з якихось причин і досі трапляються проєкти, де подібну документацію підтримують вручну. Ба більше, не всі знають про можливість генерації коду, тому клієнти для API часто пишуться вручну, використовуючи звичайну текстову документацію як вхідні дані.

Філософія, що стоїть за HATEOAS, не потребує жодних статичних визначень API, адже клієнти мають динамічно знаходити деталі, не маючи попередніх знань про структуру API. Це може бути однією з причин, чому OpenAPI «загубився» серед численних рецептів того, як бути «справді RESTful».

Я наполегливо рекомендую використовувати специфікацію OpenAPI, оскільки вона надає можливість:

  • Генерувати клієнтські бібліотеки для різних мов програмування, усуваючи необхідність вручну створювати HTTP-запити та серіалізатори (дивіться OpenAPI Generator, NSwag та інші альтернативи).
  • Генерувати серверні заглушки, забезпечуючи базову логіку маршрутизації та перевірки запитів перед тим, як реалізовувати бізнес-функціональність (якщо ви дотримуєтеся підходу «спочатку специфікація» та не генеруєте специфікації з коду сервера).
  • Генерувати документацію, яка залишається синхронізованою з реалізацією.
  • Автоматично перевіряти запити та відповіді на відповідність схемі, виявляючи проблеми інтеграції на ранніх етапах.
  • Імітувати (mock) ендпоїнти під час розробки або тестування без розгортання реального бекенду.

Хтось може поспорити, що HATEOAS усе ще актуальний, адже його динамічна структура, яка дозволяє клієнту самостійно орієнтуватися в API без знання специфікації, добре підходить для сучасних AI-агентів. Насправді ж усе навпаки. Надати штучному інтелекту специфікацію OpenAPI — це все одно, що дати йому детальну інструкцію. AI може з високою точністю розібрати визначені ендпоїнти, необхідні параметри та очікувані структури даних. Такий формальний контракт мінімізує неоднозначність і знижує ризик неправильних припущень. Натомість API, побудований за принципом HATEOAS, із динамічними посиланнями та потребою орієнтуватися у структурі відповідей під час виконання, створює набагато складніше й менш передбачуване середовище для AI, підвищуючи ризик неочікуваної поведінки.

Відповіді у випадку помилок

Є кілька типових проблем із відповідями у випадку помилок, які я часто помічаю:

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

Я не можу звинуватити Роя Філдінга в тому, що він не надав належних рекомендацій щодо обробки помилок, адже цей аспект є занадто низькорівневим для його пропозиції. Однак мене дивує, наскільки часто цей аспект ігнорується, навіть людьми з певним досвідом у розробці API.

Окрім коду стану HTTP та зрозумілого для людини повідомлення про помилку, відповідь повинна містити щонайменше:

  • Ідентифікатор помилки у вигляді рядка — код HTTP часто є неоднозначним, тому потрібен більш конкретний ідентифікатор (наприклад, InsufficientFunds).
  • Ідентифікатор кореляції (correlation identifier) — без нього неможливо знайти логи та інші записи моніторингу, що відповідають конкретній помилці.

Для належної обробки деяких помилок може знадобитися додаткова контекстна інформація.

Існує RFC 9457, який намагається стандартизувати цей підхід, але я ще не бачив його широкого впровадження, як і його попередника, RFC 7807.

Висновки

Відповідаючи на запитання «Що не так із практиками RESTful API?», я можу сказати, що головна проблема полягає в тому, що їх часто дотримуються догматично, хоча деякі з ідей з часом виявилися непрактичними.

Ідея, що всі ендпоїнти повинні бути ресурсно-орієнтованими, не має жодних переваг у більшості сценаріїв, так само як і підхід HATEOAS. Однак концепція ресурсу цілком доречна там, де вона природно вписується — наприклад, у типовому CRUD API. Коли ж вам потрібен ендпоїнт, орієнтований на дії (як у прикладі з API для завдань), я не бачу проблем у тому, щоб реалізувати її без намагання знайти ресурсно-орієнтований еквівалент.

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

Альтернативою цьому підходу є RPC поверх HTTP — і це цілком прийнятний варіант. Наприклад, перша частина URL може ідентифікувати об’єкт, остання — назву методу, а виклик здійснюватися через POST:

  • POST /api/v1/jobs/{id}/get
  • POST /api/v1/jobs/{id}/start
  • POST /api/v1/jobs/{id}/update

Як пояснювалося у розділі «Дієслова», при використанні RPC-семантики немає жодних суттєвих практичних недоліків у використанні POST для будь-якого ендпоїнта.

Специфікація OpenAPI є де-факто обов’язковим засобом для проєктування та розробки HTTP API. Вона набагато практичніша за динамічне виявлення доступних дій та ресурсів, оскільки дає змогу автоматично генерувати клієнтський код, серверні заглушки та документацію — що заощаджує час і запобігає помилкам інтеграції.

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

Ця стаття доступна також англійською на Medium.

Сподобалась стаття? Підписуйтесь на автора, щоб отримувати сповіщення про нові публікації на пошту.

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті

👍ПодобаєтьсяСподобалось6
До обраногоВ обраному5
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Вимога, щоб клієнт взаємодіяв з сервером виключно через ресурси, потрібна для ефективної підтримки HATEOAS, яка необхідна, аби будувати «тонкі» клієнти для користувача, що не містять доменних знань. Однак у більшості випадків практично неможливо створити такий клієнт без «хардкоду» доменних знань, а значна частина API взагалі не призначена для клієнтських застосунків. Виглядає так, що вимога взаємодіяти лише через ресурси не має практичної користі.

Я б ще подискутував на тему HATEOAS. Тим більше що навіть стаття на Вікіпедії на цю тему є приводом для дискусій, див. наприклад htmx.org/essays/hateoas. Принаймні у мене виникає щодо неї багато питань.

Я згоден з тим, що доменні знання про поведінку конкретних ресурсів так чи інакше повинні бути закодовані в багатьох випадках REST клієнтів.

Проте, є також багато прикладів REST API де це не потрібно, бо всі ресурси обробляються уніфіковано. Приклади — Key-Value або Document databases з REST інтерфейсом, щось типу PostgREST — уніфікований інтерфейс до реляційної бази даних, WebDAV клієнти або API для роботи з ієрархічними файловими системами, тощо.

Також, з часів Філдінга і донині існує тільки один справжній популярний hypermedia формат, це HTML.

JSON не є справжнім hypermedia форматом. Намагання зробити з нього hypermedia через специфікації типу HAL, Siren, JSON:API як на мене не були вдалими, бо вони на жаль змішують дані з метаданими, при тому що користь від цього лишається обмеженою.

Однак, використання URL як універсальних ідентифікаторів ресурсів — це та частина HATEOAS яку можливо і доречно використовувати в REST API навіть з JSON. Більшість ресурсів так чи інакше пов’язані між собою, і URL якнайкраще підходять для відображення цих зв’язків, які є важливою частиною стану ресурсів. Я б саме це і розглядав як практичне втілення HATEOAS принципу в REST.

Якщо повернутись до мого попереднього прикладу гіпотетичного `/authorization/access-control-entry/{id}` ресурсу

{
  "self": "/authorization/access-control-entry/123",
  "owner": "/users/456",
  "resource": "/documents/spreadsheets/789",
  "permissions": [
    "/authorization/permissions/READ",
    "/authorization/permissions/WRITE"
  ]
}

ми можемо побачити, що більшість стану цього ресурсу описано саме за допомогою гіперлінків (URL), що фактично і є HATEOAS. Гіперлінки є натуральним відображенням як relations в реляційній моделі так і object identity в доменній моделі.

На відміну від некваліфікованих даних "owner": 456 ми можемо одразу зрозуміти тип цього зв’язку (що це посилання саме на ресурс типу user) а також отримати однозначне розташування цього ресурсу, без необхідності покладати цей обов’язок на клієнта.

Використання URL для констант типу "/authorization/permissions/WRITE" по перше, також дозволяє кваліфікувати їх тип (наприклад відрізнити від "/editing/actions/WRITE"), отримати всі можливі варіанти допустимих значень запитом на колекцію "/authorization/permissions/", зв’язати додаткові метадані, що зручно наприклад для констант з розширеними кодами помилок — ресурс зазвичай може містити додаткові пояснення помилки.

Згоден з автором у тому що сліпо дотримуватися патернів, боьну а як же нема сенсу. Прикольно підібрані приклади

Прикольно, але тут постає event-driven архітектура і все стає зрозумілим. Бо start/stop це чудові івенти

Два варианта у меня: 1) делаешь как хочешь лиж бы работало для небольших/эксперементальных проектов это актуально. К тому же LLM отлично генирит код такой 2) для тяжелых систем лучше в рамках компании или даже для мира, разработать генератор АПИ. Задаешь ему разделы стандартные Пользователи, Запуск сервисов, Мониторинг, и т.д. а он тебе наборы АПИ, ДТО, и т.д. выдает готовое со всеми там токенами, сессиями, если было надо и т.д. Учитвает все бест практисы, именование, протоколы и т.д. Хочешь на ххтп, хочешь на грпс. На любом языке програмирования.

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

Так саме про це і стаття, що витрачати зайві зусілля на спроби зробити «все по ресту» не має практичних переваг у більшості випадків.

а кто і коли сказав що щось повинно бути по чомусь?
мабудь тількі інфоцигани-блоггери що просувають невірне формулювання RESTFull API

В натуральних мовах ми передаємо інформацію присудком (delete) + підметом (job) + «аргументами» (id=123).
В мовах програмування зводиться до того ж самого: чи то виклик методу deleteJob(id=123), чи то відправка меседжа об’єкту job.
В remote API задача та ж сама. REST-ish чи не RPC-ish — вже деталь навіть не реалізації, а «серіалізації» цих викликів.

Тому я б більше виходив з наявного тулінга і аудиторії вашого API.
Чи хочу я писати всраті OpenAPI-специфікації, якщо можу десь використати gRPC натомість? Майже ніколи.
Чи візьму я REST-ish + OpenAPI замість того, щоб придумувати власний RPC-протокол? Майже завжди.
Чи є мені різниця, як виглядає протокол, якщо мені дають класно зроблений SDK під мою мову? Ні. Аби тільки продебажити можна було у разі проблем.

В натуральних мовах ми передаємо інформацію присудком (delete) + підметом (job) + «аргументами» (id=123).
В мовах програмування зводиться до того ж самого: чи то виклик методу deleteJob(id=123), чи то відправка меседжа об’єкту job.
В remote API задача та ж сама. REST-ish чи не RPC-ish — вже деталь навіть не реалізації, а «серіалізації» цих викликів.

Саме на це я і вказую у статті. Нюанс в тому, що іноді для слідування класичним рекомендаціям для RESTful API, деякі дії ховають за зміною властивостей об’єкту, що робить семантику взаємодії з таким сервісом менш зрозумілою.

Чи хочу я писати всраті OpenAPI-специфікації, якщо можу десь використати gRPC натомість? Майже ніколи.

Повністю згоден, стаття сфокусована саме на «класичних» HTTP API, тому gRPC та інші аналоги я не згадував. Щодо OpenAPI — досить часто достатньо генерити специфікації кодом бекенду автоматично, якщо серверний фреймворк таке дозволяє. Отримуєте і плейграунд, і можливість згенерити клієнти по специфікації не витрачаючи час на написання специфікації вручну. Трохи пізніше додам в статтю більш розгорнуту думку про OpenAPI, бо схоже тут мене багато хто неправильно зрозумів.

Чи візьму я REST-ish + OpenAPI замість того, щоб придумувати власний RPC-протокол? Майже завжди.
Чи є мені різниця, як виглядає протокол, якщо мені дають класно зроблений SDK під мою мову? Ні. Аби тільки продебажити можна було у разі проблем.

І тут я повністю згоден, і стаття цьому не протирічить :) Якщо вкажете які саме формулювання призвели до непорозуміння — буду вдячним.

Кожні 5 років хтось обов’зяково підіймає цю тему в статтях чи відео) Навіть я думав про це написати, але ви випередили.
ось наприклад те саме що ви пишете із 2018 року:
www.youtube.com/watch?v=nuh35wUrJgQ

стосовно практик — немає поганих практик чи паттернів, просто вони бувають доречні чи ні.
HATEOAS — доволі непогана практика для застосування в контентних порталах чи складних системах, чому ні ? Але чи потрібна вона в будь-який системі — напевне ні.

Так я і не кажу про якісь з вказаних практик, що вони погані. Проблема саме в тому, що для розробки традиційних HTTP API, які ніколи не будуть використані в рішенні на базі HATEOAS, все одно витрачають час на «слідування традиціям». Щодо «складних систем» — це занадто неконкретне узагальнення. Я бачив щось схоже на HATEOAS в рішеннях для Data Governance, де можна навігувати графами доменних об’єктів і редагувати їх, але це дуже специфічний випадок. Також, з деякими нюансами, можу уявити як це спрацює в контент-порталах. Можна ще уявити сценарії, де кінцевий користувач взаємодіє з контентом через узагальнений UI, але в будь-якому випадку — це не мейн-стрим, тому не бачу в чому моє твердження «не має жодних переваг у більшості сценаріїв» некоректне.

Ми колись реалізовували систему де на кожну відповідь з серверу одразу приходив список можливих прав на операції над сутністю що ми отримали. Це допомогало одразу для цієї сутності будувати UI, звісно це не HATEOAS, але щось поряд.
Стосовно слідування традиціям — не бачив) якщо буде приклад дайте подивитись.

Слідування традиціям... Відразу вспливає образ старенької бабусі, яка ходить до церкви поставити свічку — бо традиція. Здавалося б — де бабця, а де середьностатистичний айтішник? Але ніт — замість релігійних традицій він вибирає ІТ-традиції і так само сліпо їм слідує. Хоча напевно це не баг, а фіча. Наскільки ж сильні дефолтні налаштування, правда? Скільки розумового ресурсу зекономило. ))) Думки вголос )))

Цей солоний аркітект в 2018 ще в школу ходив)

Senior Solution Architect в EPAM

навіть боюсь питати, але: а скільки в ЄПАМ-і рівнів у архітектів? )

как и во всех галерах такого типа — около 5 на каждый тайтл, ~1 в год можно апнуть, итого от джуна до максимума лет 20 и постоянно добавляют новые уровни для олдов

если б деньги апали так как грейды

в этом и смысл всех этих грейдов, мол, сначала добейся, а потом будем про деньги говорить

накинув, так накинув.
зачот.

Виглядає як пропаганда RPC від людини яка не дуже вникла у питання чому саме у REST такі принципи. Ну подобається вам RPC — ідіть і використовуйте GraphQL, TRPC або щось інше, навіщо ви взагалі у REST полізли, «революціонери» ?

Мова про те, що HATEOAS, який є невід’ємною частиною архітектурного стилю REST, по факту використовується в дуже обмеженій кількості випадків. В його парадигмі клієнти не мають ніяких знань про схему доменної моделі наперед. Для більшості систем, які ми всі робимо, таке обмеження значно ускладнює реалізацію не надаючи помітних переваг. А тому і не бачу сенсу суворо дотримуватися практик, які були сформульовані саме для підтримки такої парадигми.

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

Дивіться, для мене Ваша стаття є наочним прикладом ефекту Данінга-Крюгера, коли чим менше людина знає про якусь систему, тим простішою вона їй здається.

Давайте взагалі розглянемо проблему в ширшому аспекті — «Що не так з загальними практиками»?

Наведу такий простий приклад — чи можна вибросити сміття у вікно машини? Здається, в принципі ніхто за це на покарає, збережете свій час щоб не нести сміття до смітника, не захаращуватимете салон,
а те місце де ви його викинули ви вже ніколи більше проїжджати не будете, тому для вас ніяких мінусів не буде. Але через декілька років ви вирішуєте відпочити, купляєте дорогі квитки до Таїланду, приїжджаєте на чудовий пляж, і бачите там купу сміття, яку хтось колись викинув з вікна машини. Тоді ви розумієте що вибросити сміття у вікно мабуть було поганою ідеєю, але коли ви це робили, ви про це не думали з цього боку.

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

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

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

Одна з проблем в дизайні розподілених систем є стабільність контракту. Саме стабільність контракту дозволяє відносно незалежно розробляти сторони цього контракту, а також підтримувати зворотню сумісність між їх версіями. Для деяких систем стабільність є дуже суттєвим аспектом, наприклад, якщо це апі для мобільного застосунку, при зміні контракту вам треба перепубліковувати клієнтський застосунок, що є досить довгою та дорогою операцією. Ще складніше ситуація з оновленнями IoT клієнтів — часто це взагалі неможливо. Через те, що REST використовується також для інтеграції між застосунками, якщо ви додали якусь операцію до вашого контракту, її практично неможливо потім прибрати звідти через необхідність підтримання зворотньої сумісності.

Подивимось на HATEOAS, чи насправді це така обтяжуюча практика?

Давайте порівняємо два варіанти ресурсу

`/authorization/access-control-entry/{id}`

```json
{
«id»: 123,
«owner»: 456,
«resource»: 789,
«permission»: [«READ», «WRITE»]
}
```

та

```json
{
«id»: 123,
«self»: «/authorization/access-control-entry/123»,
«owner»: «/users/456»,
«resource»: «/documents/spreadsheets/789»,
«permission»: [
«/authorization/permissions/READ»,
«/authorization/permissions/WRITE»
]
}
```

Чи набагато складніше для розробника API реалізувати другий варіант порівняно з першим? Чи додає він стабільності контракту? Чи зручнішим він буде для написання клієнту? Думаю відповідь очевидна.

Стосовно використання дієслів у REST методах. Зазвичай це погана практика через те, що їх додавання погіршує стабільність контракту, а також майже завжди є ознакою поганого дизайну доменної моделі.

Через таку практику через кілька років існування замість обмеженої кількості добре зпроектованих універсальних ресурсів REST API перетворюється на купу спеціалізованих некогерентних RPC методів, доданих за запитами окремих клієнтів, кожен з яких використовується тільки цими клієнтами, а деякі взагалі не видалені тільки через зворотну сумісність.

Щодо дизайну конкретного прикладу `/api/v1/jobs/{id}/start` то він є прикладом досить
поганого дизайну доменної моделі. Оскільки сутність `job` передбачає якийсь процес, подовжений у часі, то він повинен передбачати можливість достатнього моніторингу його стану ззовні, тобто можливість оцінити його поточний та минулий стан. Простіше за все було б дизайнити `job` немутабельним, бо зміни його стану занадто ускладнюють дизайн через те, що вони також можуть бути неатомарними, бути помилковими, бути неідемпотентними, тому вимагатимуть синхронізації при конкурентному виклику, а також вимагатимуть окремого ресурсу, щоб мати можливість переглянути історію змін, типу `/api/v1/jobs/{id}/runs/{id}`

Необхідність синхронізованої зміни стану, наприклад використовуючи оптимістичне блокування, вимагає якогось REST ресурсу, до якого ми могли б надіслати умовний запит, використовуючи заголовок `If: {ETAG}`. Саме через синхронізацію коректним дизайном був би саме умовний `PATCH` або `POST` на сам `job` ресурс або його `state` subresource замість пропонованого RPC методу.

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

Необхідність в `Vary:` заголовках зазвичай викликана неправильним дизайном REST ресурсів, через наявність ресурсів типу `/my-profile` замість використання канонічних ресурсів `/profiles/{id}`. Також переважна більшість ресурсів при правильному дизайні можуть бути кешованими принаймні декілька хвилин, що навіть при помірному навантаженні дає суттєву економію часу і трафіку. Ефективність кешування можна суттєво збільшити за допомогою правильного дизайну доменної моделі, з врахуванням життєвого циклу окремих ресурсів (не змішуючи в ресурсах моделі з різним життєвим циклом).

Я б порадив розробникам REST приділяти більшу увагу вивченню неочевидних деталей HTTP протоколу, бо з моєї практики багато хто не знає деталей важливих механізмів на кшталт умовних запитів, etag, content negotiation, авторизації, заголовків кешування, cors, авторизації, компресії, деталей використання cookies, навіть кодів помилок та методів.

У ефекту Даннінга-Крюгера можуть бути різні прояви. Наприклад, коли досконало опановуєш складний молоток, весь світ починає здаватися цвяхами. З’являється ілюзія, що цим складним інструментом треба вирішувати абсолютно всі задачі, а будь-яке спрощення сприймається як некомпетентність.

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

Ви вказуєте на проблему еволюції контракту, яка існує на різних рівнях — в бібліотеках, базах даних, АПІ контрактах різної природи (не тільки HTTP), тощо. І для цієї проблеми давно існують сталі рішення і практики за межами REST.

Ще складніше ситуація з оновленнями IoT клієнтів — часто це взагалі неможливо.

Я не є експертом в області IoT, але, наскільки мені відомо, кінцеві пристрої використовують «легкі» бінарні протоколи, бо там з наявними ресурсами дуже важко і треба економити.

Тим не менш, щоб остаточно зрозуміти, який саме сенс ви вкладаєте в HATEOAS — запропонуйте, будь ласка, API для взаємодії IoT сенсора з головною системою, через яку він має посилати дані телеметрії. І поясніть, яка має бути при цьому реалізація клієнта, щоб його не треба було оновлювати при зміні протоколу.

Думаю відповідь очевидна.

Неочевидна, є багато «але». Щоб не збивати фокус с більш принципових деталей, поки залишу без додаткових коментарів.

Оскільки сутність `job` передбачає якийсь процес, подовжений у часі, то він повинен передбачати можливість достатнього моніторингу його стану ззовні, тобто можливість оцінити його поточний та минулий стан.

Я не казав, що GET непотрібен, про три методи зі статті сказано «HTTP-ендпоїнти для керування виконанням вже створеного завдання виглядали б так». Ось вам повний набір методів як я його бачу:
POST /api/v1/jobs — створити нове
GET /api/v1/jobs/{id} — прочитати
PUT /api/v1/jobs/{id} — оновити властивості (які можна оновлювати)
POST /api/v1/jobs/{id}/start — почати виконання (в тому числі після призупинки)
POST /api/v1/jobs/{id}/stop — зупинити
POST /api/v1/jobs/{id}/pause — призупинити
DELETE /api/v1/jobs/{id} — видалити

Необхідність синхронізованої зміни стану, наприклад використовуючи оптимістичне блокування, вимагає якогось REST ресурсу, до якого ми могли б надіслати умовний запит, використовуючи заголовок `If: {ETAG}`.

Поясніть, будь ласка, для чого нам потрібне оптимістичне блокування на методах зміни стану (start, stop, pause).

еобхідність в `Vary:` заголовках зазвичай викликана неправильним дизайном REST ресурсів, через наявність ресурсів типу `/my-profile` замість використання канонічних ресурсів `/profiles/{id}`

Типова ситуація: є система де користувач має бачити список проєктів компанії, але через різний рівень пермісій кожен користувач бачить різний список проєктів. Чи правильно я вас розумію, що замість /projects мені треба зробити щось на кшталт /user/{id}/projects?

Я не є експертом в області IoT, але, наскільки мені відомо, кінцеві пристрої використовують «легкі» бінарні протоколи, бо там з наявними ресурсами дуже важко і треба економити.

Якщо додати підтримку Accept-Encoding то це зробить протокол більш легким та бінарним досить малими зусиллями, при цьому зберігаючи читабельність.

Тим не менш, щоб остаточно зрозуміти, який саме сенс ви вкладаєте в HATEOAS — запропонуйте, будь ласка, API для взаємодії IoT сенсора з головною системою, через яку він має посилати дані телеметрії. І поясніть, яка має бути при цьому реалізація клієнта, щоб його не треба було оновлювати при зміні протоколу.

Зміни протоколу бувають різні. HATEOAS вирішує деякі з них але не всі, та це все одне краще ніж нічого. Ще якісь проблеми стабільності вирішує підтримка версійності ресурсів. Для телеметрії є стандартні протоколи типу MQTT

Поясніть, будь ласка, для чого нам потрібне оптимістичне блокування на методах зміни стану (start, stop, pause).

Як один з варіантів вирішення проблем консистентності при конкурентній зміні даних.

Наприклад:

  1. Ви зробили GET /api/v1/jobs/6
  2. Хтось зробив PUT /api/v1/jobs/6 щоб поміняти наприклад job.description. Тут до речі цікаве питання, як ви плануєте реалізувати PUT? Чи дозволено через PUT міняти статус? Якщо так, навіщо тоді методи start та stop? Якщо ні, чи це не зайве ускладнення, забороняти міняти статус через PUT та додавати окремі методи для цього?
  3. Ви зробили POST /api/v1/jobs/6/stop. Припустимо, сервер повернув 200 ОК (чи скоріше 202 Accepted або 204 No Content бо stop це не ресурс і не має окремої репрезентації)
  4. На вашому клієнті цей ресурс має неконсистентний стан, бо він «не побачив» зміни job.description, тільки зміни статусу, що він робив сам
Типова ситуація: є система де користувач має бачити список проєктів компанії, але через різний рівень пермісій кожен користувач бачить різний список проєктів. Чи правильно я вас розумію, що замість /projects мені треба зробити щось на кшталт /user/{id}/projects?

Так, наявність user id в локаторі ресурсу вирішить проблему з некоректним кешуванням. Чи робити projects залежним ресурсом від user залежить більше від взаємовідношення та життєвих циклів цих ресурсів.

Наприклад в типовому трекері задач проекти можуть існувати незалежно від користувачів, тому це будуть дві колекції верхнього рівня. Тоді проблему кешування вирішить щось типу /projects?user={userid}

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

Ще один нюанс, що запит на колекцію /projects повертає не список ресурсів типу project, а список ресурсів типу list-item. Через це зміна стану окремого проекту не інвалідує в кеші всю колекцію, тільки додавання чи прибирання елементів інвалідує, що трохи покращує ефективність кешування.

Якщо додати підтримку Accept-Encoding то це зробить протокол більш легким та бінарним досить малими зусиллями, при цьому зберігаючи читабельність.

Accept-Encoding вирішує лише проблему ширини каналу. Для IoT пристрою вузьким місцем часто є не мережа, а CPU та RAM. Розпарсити стиснутий JSON, проаналізувати HATEOAS-посилання і вирішити, куди йти далі — це на порядок важча операція, ніж прочитати байти по фіксованому зміщенню в бінарному протоколі.

Зміни протоколу бувають різні. HATEOAS вирішує деякі з них але не всі, та це все одне краще ніж нічого. Ще якісь проблеми стабільності вирішує підтримка версійності ресурсів. Для телеметрії є стандартні протоколи типу MQTT

Фраза «краще ніж нічого» — це не інженерний аргумент. Щодо HATEOAS в телеметрії Ви так і не навели приклад, як саме клієнт має «адаптуватися». Якщо зміна протоколу вимагає перепрошивки пристрою, то HATEOAS там був лише зайвим вантажем.

Згадка вами MQTT дуже доречна — вона якраз підтверджує мою тезу: для специфічних задач ми беремо специфічні інструменти, а не намагаємося всюди використовувати REST.

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

Чи дозволено через PUT міняти статус?

Ні, не дозволено. Зміна статусу є результатом дії а не її тригером.

На вашому клієнті цей ресурс має неконсистентний стан, бо він «не побачив» зміни job.description, тільки зміни статусу, що він робив сам

Оптимістичне блокування призначене для запобігання неконсистентності збереженого стану, і тут зміна опису і зупинка завдання один одному не заважають, тому не потребують блокування між собою. Те що ви кажете про клієнт не є проблемою — якщо хтось інший натисне «Стоп» через мілісекунду після того як завершилася зміна опису, я все одно не побачу зміну статусу, якщо не відбудеться ручного чи автоматичного оновлення поточного стану на клієнті.

Так, наявність user id в локаторі ресурсу вирішить проблему з некоректним кешуванням.

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

Чудова стаття! Повністю погоджуюся зі всім озвученим

Як я розумію, первинна ідея HATEOAS полягала не в тому, щоб клієнт міг «інтуїтивно досліджувати» незнайомий API. Сенс був у тому, щоб клієнт працював із символічними лінками, які повертає бекенд, і саме назви цих лінків є частиною контракту. Натомість реальні URL-адреси за цими лінками не повинні бути для клієнта значущими — бекенд має право змінювати їх без необхідності оновлювати клієнт.
Тобто URL-адреси — це внутрішня деталь реалізації, а символічні лінки — це стабільний інтерфейс. Це свого роду інкапсуляція: клієнт знає про «гетери» (іменовані лінки), але не покладається на «поля» (конкретні URL).

Щодо використання пост-запитів з командами (POST на ендпоінт-команду, що описується дієсловом), то це також є частиною загальноприйнятої практики, що описується в багатьох керівництвах, наприклад: https://dotnet.rest/docs/bestpractises/post-vs-put/

3. Triggering operations that don’t fit the resource model (controller resources)

POST /api/tasks/42/send-reminder HTTP/1.1
{
  "recipients": ["[email protected]"]
}
Як я розумію, первинна ідея HATEOAS полягала не в тому, щоб клієнт міг «інтуїтивно досліджувати» незнайомий API. Сенс був у тому, щоб клієнт працював із символічними лінками, які повертає бекенд, і саме назви цих лінків є частиною контракту. Натомість реальні URL-адреси за цими лінками не повинні бути для клієнта значущими — бекенд має право змінювати їх без необхідності оновлювати клієнт.

Якщо ви про «Хтось може поспорити, що HATEOAS усе ще актуальний, адже його динамічна структура, яка дозволяє клієнту самостійно орієнтуватися в API без знання специфікації, добре підходить для сучасних AI-агентів» — то це тільки приклад думки, яку я зустрічав як виправдання HATEOAS. Основні мої власні міркування про HATEOAS я написав в розділі про ресурси і вони не протирічать написаному вами.

Щодо використання пост-запитів з командами (POST на ендпоінт-команду, що описується дієсловом), то це також є частиною загальноприйнятої практики, що описується в багатьох керівництвах, наприклад: https://dotnet.rest/docs/bestpractises/post-vs-put/

Воно все так, але формально цей підхід протирічить «канонам REST», адже ендпоїнт стає не ресурс-орієнтованим, і тому не всі на нього погоджуються з релігійних міркувань. Саме тому я в статті хотів підкреслити, що це цілком нормальний варіант.

Головна помилка вивчаюючих

канони REST

є думати що існують канони REST.
REST — це набір практик для HTTP API, не більше.
Те що ми розробляємо це не обов’зяквово API що реалізує паттерни REST, це HTTP API, просто ютубери, блоггери та інфобізнесмени вигадали поняття RESTfull, і просували його, що було не вірно.

Я не просто так ці «канони» взяв в лапки, але, тим не менш, у терміна є першоджерело. В ньому є набір архітектурних обмежень, але про прив’язку до HTTP нічого не говориться, тому що сам концепт високорівневий і protocol-agnostic. Відповідно, твердження

REST — це набір практик для HTTP API, не більше.

фактично некоректне.

А ось з цим твердженням погоджусь:

Те що ми розробляємо це не обов’зяквово API що реалізує паттерни REST, це HTTP API, просто ютубери, блоггери та інфобізнесмени вигадали поняття RESTfull, і просували його, що було не вірно.

І для розробки зрозумілого HTTP API яке легко використовувати і підтримувати, зовсім необов’язково слідувати усім «заповідям» які асоціюють з терміном RESTful, про що власне і стаття.

Якщо звернутись до оригіналу «Architectural Styles and the Design of Network-based Software Architectures by Roy Thomas Fielding, 2000.»

то Рой пише буквально:

«The Representational State Transfer (REST) style is an abstraction of the architectural elements within a distributed hypermedia system. REST ignores the details of component implementation and protocol syntax...»

То є так, формально він визначає це як принципи, що не зав’язані на протоколі. Бо це наукова стаття тут не можна змішувати теорію з практикою.

Але які ви знаєте реально працюючі гіпермедіа протоколи окрім HTTP ? Можно сказати що є Gemini на декілька тисяч сайтів, все інше легасі непрацююче або експерементальне.

Далі в його ж роботу описується як підходи REST використовуються в протоколі HTTP.

«Since 1994, the REST architectural style has been used to guide the design and development of the architecture for the modern Web».

Тому наведіть приклади в свої практиці (а не теорїї) де ви окрім HTTP API або його надбудов використовували архітектурний стиль REST.

Чому не зробити наступне (воно ще й буде простіше розширятись):

POST /api/tasks/42/reminders

Можна в тіло додати ще поле «відправити негайно».

то це також є частиною загальноприйнятої практики, що описується в багатьох керівництвах

Хто такий Benjamin Abt? І чому те що він описав є «частиною загальноприйнятої практики», особливо поза дотНет світом?
Проблема в тому, що в контексті РЕСТ немає актуального авторитетного канону. Є дисертація Філдінга, є статті Фаулера і Ко, але вони описують загальніші та абстрактніші речі. Використання дієслів в УРЛ раніше вважалось антипатерном.

Якщо про загальноприйняті, то ось тут є спроба від Microsoft (без прив’язки до «світу .NET») описати власні стандарти щодо розробки REST API
https://github.com/Microsoft/api-guidelines/blob/master/Guidelines.md
Зокрема в описі POST явно згадується submit a command:

POST - Create a new object based on the data provided, or submit a command
Тобто все, що можна вкласти в стандартну семантику CRUD операцій над ресурсами, має описуватись HTTP дієсловами (GET/POST/PUT/DELETE/...). Але в решті випадків не має бути проблемою описувати ендпоінти-команди, які не вписуються в схему CRUD-операцій (тобто після зваженого рішення, що ця команда не може бути природно описана в термінах CRUD над якимось ресурсом).
Хоча в тому ж документі описується доволі специфічний флоу для опису Operations resource, але намагатись натягнути його на кожний випадок керування станом через «довготривалі» команди я б мабуть не радив.

Такі випадки, як використання POST /login та POST /logout замість чогось РЕСТового на кшталт POST /auth та DELETE /auth виглядають цілком загальнозрозумілими. Натомість щось типу POST /user/123/reset-password цілком можна було б описати і по РЕСТовому: PUT /user/123/password, бо тут є семантика CRUD над ресурсом, але це вже справа смаку.

Може не дуже слідкував за дискусією щодо Hateoas.

Коротко: це ідеал, який не працює.

Виключно мої думки: сервер забов’язаний по будь якому попередьому uri обробляти надіслана інформацію. Бо, якщо він перевантажений, то він банально не в змозі повернути uri іншого серверу. Балансування навантаження повинно відбуватись на рівнень вище за application level. Це не проблема клієнта, в ідеалі кліент може використовувати uri з респонсу, але не обов’язково. Може клієнт хардкодить uri? Може, бо це проблеми «серверу» зробити добре всім, але не залежить як endpoint буде обробляти навантаження на backend nodes. Клієнт може надіслати запит на будь який сервер, який він знає і отримати відповідь (з розрахунком його timeouts).

HATEOAS
Не працює воно в реалі. Бо нема сенсу.

Зокрема в описі POST явно згадується submit a command:

Гайди МС — це вже краще ніж приватний сайт якоїсь людини.
Але питання не в матоді, а в УРЛ. Те що для команди треба ПОСТ — це логічно (може навіть в «святому писанні» є). Проблема в «send-reminder» — це чистий рівень 0 в моделі Річардсона.

Натомість щось типу POST /user/123/reset-password цілком можна було б описати і по РЕСТовому: PUT /user/123/password

Знову ж, це не «по ресту», а просто «спроба мапити все по максимуму на дієслова ХТТП». Особливо це не по ресту, якщо ресет пароля відпраляє лист, тобто операція не дуже ідемпотентна.

Такі випадки, як використання POST /login та POST /logout замість чогось РЕСТового на кшталт POST /auth та DELETE /auth виглядають цілком загальнозрозумілими.

Знову ви демонструєте типову місконцепцію того що таке РЕСТ.
«DELETE /auth» — має видалити ресурс auth, після цієї операції ніхто вже не зможе до нього звернутись :) В такому дизайні у вас 1 аутентифікація на всю системи, фактично 1 користувач.
Логаут можна реилізувати як «DELETE /auth/123» — але має свої логічні недоліки.

Тобто все, що можна вкласти в стандартну семантику CRUD операцій над ресурсами,

Отакий підхід — найбільша проблема в розумінні РЕСТ. РЕСТ — це не про КРУД, це окрема модель, вона потребує прийняття її логіки, а не спроб натягнути вже відомий користувачу підхід на її концепції.
Тут схожа проблема з людьми, що пробують натягнути різні парадигми програмування одна на одну, як то ФП на ООП.

Ідея, що всі ендпоїнти повинні бути ресурсно-орієнтованими, не має жодних переваг у більшості сценаріїв, так само як і підхід HATEOAS
RPC поверх HTTP — і це цілком прийнятний варіант

HATEOAS — це чудова ідея, що не злетіла з цілого ряду причин. Одна з яких — це власне те, що мало хто будує розподілені системи, де потрібно «управління клієнтом з сервера».
Щодо ресурсно-орієнтованості, то її основна перевага — зрозумілість для команд клієнтів, коли сервер не надає «клієнтську бібліотеку». Але це також залежить від того наскільки ви прив’язуєте ваше АПІ до РЕСТ як архітектурного стилю і як результат варіативність АПІ, яку ви хочете мати. Наприклад, чи хочете ви мати можливість віддавати дані у різних форматах (ХМЛ на додачу до ДжСОН).
RPC поверх HTTP — це не стільки про принятність, скільки про конкретний сценарій використання. Якщо у вас сценарій під RPC, то чому йому бути не прийнятним. Але нижче Nikita Podgorbunskyi запропонував ресурсно-орієнтоване АПІ, яке мені дещо зрозуміліше ніж запропоноване вами. Я не бачу об’єктивних/вимірюваних критеріїв чому треба обрати те чи інше.
Одна крайність — це пуризм (в нашому випадку щодо РЕСТ), але інша — це відкидання всіх практик, як непотрібних, мотивуючи істуванням альтернативного підходу.

Хоча специфікація OpenAPI (раніше Swagger) існує вже давно... Ба більше, не всі знають про можливість генерації коду

А тепер хвилинка крику і болю: OpenAPI — гівно!!!
Знову ж для простих випадків, особливо якщо цільова мова — джава або щось подібне до неї, воно чудово вирішує проблему, як бонус ще ховає реалізацію за згенерованим клієнтом.
Але складності починаються, коли у вас складні випадки.
Кілька прикладів:
— Клієнт — це компайл тайм залежність. Клієнтська команда не може почати розробку поки не отримає бібліотеку. Вносити ламаючі зміни стає складніше, навіть тоді коли цієї складності не має бути — клієнтська і серверна команди тісно працюють і мають синхронізовані спринти, тому можна було б просто домовитись «ми замінемо поля, функція буде зламана кілька днів, потім ви їх підтримаєте на своїй стороні»
— Комбінації схем (oneOf, anyOf, allOf, not). Воно не працює нормально навіть в мовах де підтримуються такі типи, а в джаваподібних то взагалі пекло. Навіть простий сценарій: маємо базову сутність де заборонені додаткові поля і нам потрібно для іншого ендпоінту створити таку саму але з 1 додатковим полем.

Щодо ресурсно-орієнтованості, то її основна перевага — зрозумілість для команд клієнтів, коли сервер не надає «клієнтську бібліотеку». Але це також залежить від того наскільки ви прив’язуєте ваше АПІ до РЕСТ як архітектурного стилю і як результат варіативність АПІ, яку ви хочете мати. Наприклад, чи хочете ви мати можливість віддавати дані у різних форматах (ХМЛ на додачу до ДжСОН).

Я не розумію що буде незрозумілого для команд клієнтів, якщо сервер буде виставляти ендпоїнти в RPC стилі, а не в ресурсному. Формати — взагалі ортогональне питання, можна керувати параметрами та заголовками незалежно від того ресурсно-орієнтована семантика чи RPC.

Одна крайність — це пуризм (в нашому випадку щодо РЕСТ), але інша — це відкидання всіх практик, як непотрібних, мотивуючи істуванням альтернативного підходу.

Я у висновках як раз і пишу «Ідея, що всі ендпоїнти повинні бути ресурсно-орієнтованими, не має жодних переваг у більшості сценаріїв». Тобто я не кажу що ресурсна орієнтованість це в цілому погана практика, а наголошую на тому, що якщо зручніше щось зробити без неї — go for it.

Але нижче Nikita Podgorbunskyi запропонував ресурсно-орієнтоване АПІ, яке мені дещо зрозуміліше ніж запропоноване вами. Я не бачу об’єктивних/вимірюваних критеріїв чому треба обрати те чи інше.

Тут я зрозумів що можливо недостатньо чітко окреслив скоуп прикладу. Я навів приклади ендпоїнтів тільки для стану виконання завдання (запуск, зупинка, пауза), а не всього життєвого циклу самого об’єкту (створити, прочитати, видалити). Завтра напишу детальніше, може статтю трохи оновлю.

А тепер хвилинка крику і болю: OpenAPI — гівно!!!

У нього є недоліки, але для мейнстрім АПІ зрілих поширених альтернатив небагато :-)

— Клієнт — це компайл тайм залежність. Клієнтська команда не може почати розробку поки не отримає бібліотеку.

Можна робити specification-first і генерувати бібліотеку для клієнта і стаби для сервера зі специфікації.

— Комбінації схем (oneOf, anyOf, allOf, not). Воно не працює нормально навіть в мовах де підтримуються такі типи, а в джаваподібних то взагалі пекло.

Є таке, тому іноді набір використаних конструкцій з цієї специфікації має сенс обмежувати.

Я не розумію що буде незрозумілого для команд клієнтів, якщо сервер буде виставляти ендпоїнти в RPC стилі, а не в ресурсному. Формати — взагалі ортогональне питання, можна керувати __параметрами та заголовками__ незалежно від того ресурсно-орієнтована семантика чи RPC.

Це скоріше мінорний момент, проте на етапах ознайомлення він може дещо зменшити час.
Configuration vs convention. РЕСТ АПІ має певні кращі практики, люди їх зазвичай знають. У випадку РПС ви власне навели частину проблеми — параметром чи заголовком передавати очікуваний формат? РЕСТ дає «типову відповідь».

Тут я зрозумів що можливо недостатньо чітко окреслив скоуп прикладу. Я навів приклади ендпоїнтів тільки для стану виконання завдання (запуск, зупинка, пауза), а не всього життєвого циклу самого об’єкту (створити, прочитати, видалити)

Ось цей пункт значно цікавіше. Навіть в контексті «виконання завдань» виникає питання розширення функціонали — dou.ua/...​818/?from=fortech#3035585

Можна робити specification-first і генерувати бібліотеку для клієнта і стаби для сервера зі специфікації.

Для певних проектів — це топ, бо обмежує і контролює і тих хто імплементує клієнтську логіку і тих хто імплементує обробку на серверній стороні. З точки зору архітектора, якому треба спустити кільком командам контракт — це дуже круто.
Але якщо у нас проект невеликий, команди працюють тісно і немає потреби контролювати, що вони не налажають, то ми втрачаємо гнучкість.

Для мене значно очевиднішими були б такі ендпоінти

POST /api/v1/jobs/ + {body} return {job + id} (створюємо/додаємо задачу)
GET /api/v1/jobs/{id} return {job} (перевіряємо статус задачі)
PUT /api/v1/jobs/{id} + {body} return {job + id} (оновлюємо задачу можливо переведеням на паузу)
DELETE /api/v1/jobs/{id} return {job + id} (просто вертаємо об’єкт який видалили іноді це корисно чим просто вертати тільки хедер зі статусом)

Приблизно так і працює частина апі OpenAI припіром platform.openai.com/docs/api-reference/runs хоча і частина підходів зі статі теж має місце

До речі, цей сценарій гарно демонструє одну з переваг ресурсів — можливість розширення.
Коли треба буде додати операції поновлення роботи після паузи, архівування, русного рестарарту чи ретраю, зміни пріоритету і тд, то
— в РПС підході — це окремі ендпоінти і відповідно купа згенерованих клієнтів;
— з ресурсами — це додавання нового статусу в існуючий ПАТЧ

А ви розумієте, що у вас у класичному «розшвирюваному» підході в коді сервера потрібно буде мати десятки if-ів щоб покрити всі можливі комбанації переходу з одного статусу в інший, зʼявиться по вимогах новий статус, треба буде ще N-ну кількість if-ів написати, і все це всього лиш для одного статусу. Розширюваність капітальна))

Так у вас так і так буде десь розгалуження логіки лише питання де саме. Так чи інакше доведеться реалізовувати state machine тут лише питання який інтерфейс взаімодії реалізувати.

тут різниця не де саме, а як саме, якщо йти «RPC шляхом», то 1 один новий статус/дія це 1 новий ендпоінт, а якщо класичним, то 1 новий статус/дія це N нових if-ів. Який з цих 2-х способів буде легше імплементувати?

+ ще одна перевага «RPC шляху» це те що з path ендпоінту буде очевидно що він зробить, навіть якщо людина той ендпоінт не писала, а не гадати по пейлоаду що має відбутися

Ну десь в коді у вас буде if або по частині url або по атрибуту body ото і вся різниця за великим рахунком. Як вже казав вся складність так чи інакше буде лягати на state machine яка в ідеалі не має залежати від того в якому саме форматі прийшов запит. Чи це крон джоба, чи це виклик апі ендпоінта чи може евент в pub sub підході.

якщо буде if по частині URL і все в одному хендлері, то так, це буде каша, а якщо це буде по хендлеру на кожну дію, то це вже зовсім інша справа, ми будемо точно знати, що по такій то частині URL у нас очікується такий то пейлоад, і буде своя логіка
А якщо робити в одному ендпоінті і читати параметри в body, то вам треба буде:
1. Провалідувати всі можливі дії,
2. Провалідувати чи пейлоад підходить під певну дію,
3. Провалідувати, чи якісь дії одночасно можливі,
4. Вам це все треба буде якось задокументувати для клієнта, щоб було зрозуміло механіку роботи, бо воно взагалі не очевидно без доки
5. І т.д.

Логіка складніша просто в рази, і все заради чого, того що буде менше ендпоінтів?

1. Провалідувати всі можливі дії,
2. Провалідувати чи пейлоад підходить під певну дію,
3. Провалідувати, чи якісь дії одночасно можливі,
4. Вам це все треба буде якось задокументувати для клієнта, щоб було зрозуміло механіку роботи, бо воно взагалі не очевидно без доки
5. І т.д.

Частину з цього вирішує ОпенАпі :) Особливо круто воно працює з тайпскріптом де 1, 2, 4 вирішуються на рівні згенерованих типів (я перевіряв).
Окрім напевне пунткту 3, який треба буде все одно, бо ця валідиція вам потрібна навіть якщо у вас окремі ендпоінти

Яким чином Open API вирішує імплементацію логіки?
У TS це буде зовсім не круто, вам треба буде перш ніж валідувати заматчити який саме валідатор накладати, а враховуючи, що у TS/JS відсутній патерн матчинг це буде ой як незручно, не знаю що ви там пробували
З окремими ендпоінтами я буду чітко розуміти які саме валідатори використовувати на Body що прийшло, а не плодти череду if-ів щоб зрозуміти що це

Яким чином Open API вирішує імплементацію логіки?

В який з пунктів ви включили імплементацію логіки?

У TS це буде зовсім не круто, вам треба буде перш ніж валідувати заматчити який саме валідатор накладати,

Якраз валідацію реквесту робить ОпенАпі, чи може то якась ліба.
Якщо ви про валідацію можливості переходу в певний стан, то це вам треба незалежно від обраного підходу робити руками на початку обробки певного стану (на цьому етапі ви вже знаєте конкретну схему з якою працює ваш хендлер)

В який з пунктів ви включили імплементацію логіки?

1, 2, 3.

Якраз валідацію реквесту робить ОпенАпі

Яким чином специфікація пише за вас код?

Якщо ви про валідацію можливості переходу в певний стан, то це вам треба незалежно від обраного підходу робити руками на початку обробки певного стану (на цьому етапі ви вже знаєте конкретну схему з якою працює ваш хендлер)

на цьому моменті так, у вашому підході головний недолік це лишня важкість в імплементації того, що йде до цього

В який з пунктів ви включили імплементацію логіки?
1, 2, 3.
2. Провалідувати чи пейлоад підходить під певну дію,

Валідація пейлоада не включає в себе імплементацію.

Якраз валідацію реквесту робить ОпенАпі
Яким чином специфікація пише за вас код?

Кодогенерація чи 3-парті ліба. Ключове — розробник не пише валідацію руками.

на цьому моменті так, у вашому підході головний недолік це лишня важкість в імплементації того, що йде до цього

До цього йде — 1 світч або гет з мапи + код бібліотеки (кодогенерація чи 3-строння).

Кодогенерація чи 3-парті ліба. Ключове — розробник не пише валідацію руками.

А хто пише? Яка кодогенерація наклепає вам валідацію? Ви пропонуєте спочатку писати Open API, а потім з неї генерувати серверний код для валідації?

До цього йде — 1 світч або гет з мапи + код бібліотеки (кодогенерація чи 3-строння).

Нащо писати свіч чи тягнути 3-rd party якщо можна все це зробити через роутер вебсерверу?
Що ви виграєте своїм підходом? Мій підхід наприклад виграє час імплементації і простоту розуміння/підтримки/документації даного коду

А хто пише? Яка кодогенерація наклепає вам валідацію? Ви пропонуєте спочатку писати Open API, а потім з неї генерувати серверний код для валідації?

Почнемо з базового питання:
Чи знаєте ви, що на основі ОпенАпі схеми:
1) Можна згенерувати тайпсрипт типи?
2) Можна зробити загальну валідацію відповідності об’єкта певній схемі без ручного написання коду?

Якщо так, то мені не зрозумілі ваші питання. Якщо не знали, то тепер знаєет

1) Навіщо мені генерувати TS з OpenAPI якщо значно зручніше зробити навпаки? Чули про code first?
2) Для цього є багато валідаційних ліб, але перш ніж застосувати одну зі схем, то треба по якомусь критерію її обрати, я обираю HTTP роутер для цього, бо мені не треба буде для цього писати свою машинерію

Краще щось не писати, ніж писати, тайм ту маркет, не знали, то тепер знатимете

1) Навіщо мені генерувати TS з OpenAPI якщо значно зручніше зробити навпаки? Чули про code first?

Удачі з таким підходом, коли у вас сервіси на різних мовах і підтримуються різними командами, а ще є архітектурна команда, яка визначає або хоч якось впливає контракт між сервісами.

2) Для цього є багато валідаційних ліб, але перш ніж застосувати одну зі схем, то треба по якомусь критерію її обрати, я обираю HTTP роутер для цього, бо мені не треба буде для цього писати свою машинерію

Так і тут вам не треба писати машинерію.
У вас валідатор вбудовується мідлвейр, там відбувається валідація власне об’єкта в тілі, разом з іншими частинами специфікації описаної в ОпенАпі. Критерій — ендпоінт, далі йде валідація, що тіло це один з типів (в нашому випадку це або Пауза, або Рестарт, або Стоп і тд).
В чому проблема? Де тут написання машинерії?

А ви розумієте, що у вас у класичному «розшвирюваному» підході в коді сервера потрібно буде мати десятки if-ів щоб покрити всі можливі комбанації переходу з одного статусу в інший

На щастя, я знаю, що таке стейтмашина і скоріше за все не буду писати спагетті код :)
А в описаному випадку все взагалі без десятків іфів, бо там достатньо 1 світч чи мапи де статус є ключем, а значенням операція.

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

ви знаєте що таке стейт машина, але не знаєте, що чим менша функція, тим краще.

Звісно незнаю, бо це типова помилка недосвідчених розробників вважати, що чим менша функція, тим краще.
Але навіть якщо відкинути це, то не ясно чому функція має збільшитись, коли у вас однакова обробка стану + додаткова функція, яка проводить вибір стану.

Окей, допустимо так зміну статусу ви похендлите, що якщо додасться ще якась дія над ресурсом, теж будете це пихати в один ендпоінт?

А в чому проблема (за умови, що ця операція логічно туди вписується)? В норм реалізованій стейтмашині — це 1-2 строки коду.

Мені тут про досвід втирає людина, яка каже, що краще запхнути все в одну функцію і робити стейт машину щоб все це захендлити, тобто фактично імплементовувати роутер, замість того, щоб скористатися роутером ліби/фреймворку?)
Ну так, так, будемо витрачати час, щоб написати гарно реалізовану стейт машину, замість того, щоб просто користатися роутером

Мені тут про досвід втирає людина, яка каже, що краще запхнути все в одну функцію

А хто саме вам окрім мене про досвід втирав? Бо цікавий факт — я не казав все запхнути в 1 функцію, а робити ручний діспатч (це нормальна практика)

фактично імплементовувати роутер, замість того, щоб скористатися роутером ліби/фреймворку?)

Ви точно розумієте різницю між стейтмашиною/ручним діспатчем та реалізацією обробки ХТТП запитів? В стейтмашині вам треба парсити руками пейлоад, хідери, читати параметри з УРЛ та вибирати по складним правилам, а не просто по рівності.

Ну так, так, будемо витрачати час, щоб написати гарно реалізовану стейт машину, замість того, щоб просто користатися роутером

Хм. Опишіть затрати на додавання нового стану в роутер та в стейт машину.
Нагадую, що в стейтмашині вам не треба додавати мідлвейр.

А хто саме вам окрім мене про досвід втирав? Бо цікавий факт — я не казав все запхнути в 1 функцію, а робити ручний діспатч (це нормальна практика)

От ви і про досвід втираєте якусь дикуху. Так нащо ту функцію зі свічем робити, якщо можна не робити?

Ви точно розумієте різницю між стейтмашиною/ручним діспатчем та реалізацією обробки ХТТП запитів? В стейтмашині вам треба парсити руками пейлоад, хідери, читати параметри з УРЛ та вибирати по складним правилам, а не просто по рівності.

Нащо мені морочити собі голову стейт машиною і ручним диспатчем, якщо я можу зробити по ендпоінту (хендлеру) на кожну дію з ресурсом і в кожному хендлері окремо прописувати логіку і клієнт сам буде обирати ендпоінт в залежності від дії яку йому треба зробити? Ви займаєтеся оверінженірінгом заради якоїсь непрагматичної ідеї

Хм. Опишіть затрати на додавання нового стану в роутер та в стейт машину.
Нагадую, що в стейтмашині вам не треба додавати мідлвейр.

Вам цю стейт машину спершу треба написати перш ніж додавати в неї якісь рядки, до чого тут взагалі мідлвери?

Так нащо ту функцію зі свічем робити, якщо можна не робити?

Для того, щоб спростити менеджмент АПІ.

Нащо мені морочити собі голову стейт машиною і ручним диспатчем, якщо я можу зробити по ендпоінту (хендлеру) на кожну дію з ресурсом і в кожному хендлері окремо прописувати логіку

Бо в якийсь момент у вас постане проблема онбордингу нових клієтів і пояснення нам, що наша стейтмашина ось така і ось 100500 едпоінтів або методів в клієнтській бібліотеці де вам треба знати потрібний.
А потім постане проблема схожих ендпоінтів, наприклад Старт та Рестарт або Резюм і додавання нових параметрів у декілька місць. А потім треба буде очистити ваше АПІ, і у випадку з операцією в полі у вас буде компайл тайм помилка, а у випадку з УРЛ — ну йдіть і шукайте хто його викликає і формує наприклад конкатинацією строк (тобто лише в рантаймі ви про то дізнаєтесь). Згенерований клієнт (а не лише типи даних) вирішує останню проблему, але його не завжди можна використати (на відміну від генерації типів для пейлоада).

Я переписав речення у статті на «Найочевидніші HTTP-ендпоїнти для керування виконанням вже створеного завдання виглядали б так», щоб акцент був зрозумілішим. Ваші приклади POST/GET/DELETE тут нерелевантні, бо мова йде про керування виконанням.

Щодо

PUT /api/v1/jobs/{id} + {body} return {job + id} (оновлюємо задачу можливо переведеням на паузу)

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

З точки зору клієнта який є чи користувачем, що *натискає кнопку* «старт» або «пауза», чи автоматизацією, якій треба виконати *дію*, це не оновлення властивості, а конкретна дія — «почати виконання», «призупинити виконання», тощо.

Тобто в вас і у клієнта це дії, і на сервері — окремі функції під кожен перехід стану. Нащо між ними додавати штучну абстракцію через оновлення властивості? Що це дає практично?

Інший аспект — наскільки очевидним буде такий АПІ для девелопера або для якого-небудь АІ агента. Коли читаєш окремі методи під типові дії — все видно відразу, а коли цілий набір доступних дій прихований під зміною властивостей — можуть виникати питання.

І ще одне — а що якщо нам знадобляться параметри у таких операцій — наприклад, причина зупинки чи паузи?

Ваші приклади POST/GET/DELETE тут нерелевантні, бо мова йде про керування виконанням.

GET — повністю актуальний бо навіть в прикладі що я навів саме так вони це і роблять, ви без цього ніяк. Щоб керувати станом вам майже 100% треба буде знати в якому зараз стані задача
DELETE — чому ні якщо треба відмінити задачу
POST — можливо не треба якщо ми не створюємо саму задачу, але не рідко його використовують як для створення так і для апдейта просто в залежності чи надається ІД чи ні виконуються різні дії.

В даному випадку зміна стану — це фізично не просто оновлення поля, а цілий набір дій з «виконавцем» — чи то системний процес, джоба в k8s, чи ще щось.

Та в будь якому CRUD інтерфейсі частина яка U це завжди найбільший шматок коду. Хоча легко може бути і просто апдейтом десь в базі, а далі вже якийсь воркер підхопить зміни і або виконає або відправить запит на виконання. Те що далі може бути прорва роботи то інша тема, я лише про ту частину яка формує зовнішній інтерфейс.

З точки зору клієнта який є чи користувачем, що *натискає кнопку* «старт» або «пауза», чи автоматизацією, якій треба виконати *дію*, це не оновлення властивості, а конкретна дія — «почати виконання», «призупинити виконання», тощо.

Але ж в черзі у вас задачі будуть мати цілком конретні статуси щоб ваш код який їх виконує завжди знав на чому він зупинився і яка частина системи повинна далі з ним працювати.

Тобто в вас і у клієнта це дії, і на сервері — окремі функції під кожен перехід стану. Нащо між ними додавати штучну абстракцію через оновлення властивості? Що це дає практично?

Формально я не можу сказати чи один підхід кращий за інший, але якщо ми додаємо якісь дії в path частину url це певною мірою розходиться з тим до чого люди звикли. Приміром я завжди був противником додавати параметри пошуку в path бо воно хоча і працює, але варіантів імплементації так багато що кожен хто це буде потім підтримувати мусить вивчати з нуля реалізацію.

Інший аспект — наскільки очевидним буде такий АПІ для девелопера або для якого-небудь АІ агента. Коли читаєш окремі методи під типові дії — все видно відразу, а коли цілий набір доступних дій прихований під зміною властивостей — можуть виникати питання.

Вони у вас в будь якому випадку виникнуть при роботі з новим інтерфейсом

І ще одне — а що якщо нам знадобляться параметри у таких операцій — наприклад, причина зупинки чи паузи?

Та все що завгодно можна додати в body.

{
"status": "pause",
"desc": "reason" 
}
GET — повністю актуальний бо навіть в прикладі що я навів саме так вони це і роблять, ви без цього ніяк. Щоб керувати станом вам майже 100% треба буде знати в якому зараз стані задача
DELETE — чому ні якщо треба відмінити задачу
POST — можливо не треба якщо ми не створюємо саму задачу, але не рідко його використовують як для створення так і для апдейта просто в залежності чи надається ІД чи ні виконуються різні дії.

Коли я підкреслив «для керування виконанням вже створеного завдання», я мав на увазі тільки методи які змінюють стан виконання.
DELETE в цьому прикладі не підходить для зупинки задачі, тому що зупинка і видалення — це різні операції. Після зупинки я хочу залишити задачу в історії, щоб можна було подивитися дані по завданню і якщо що — запустити нове з такими самими вхідними даними. Те на що вказуєте ви — це специфічний випадок, де зупинка/відміна еквівалентна видаленню.

Якщо що, я б робив повний набір методів для свого прикладу таким:

POST /api/v1/jobs — створити нове
GET /api/v1/jobs/{id} — прочитати
PUT /api/v1/jobs/{id} — оновити властивості (які можна оновлювати)
POST /api/v1/jobs/{id}/start — почати виконання (в тому числі після призупинки)
POST /api/v1/jobs/{id}/stop — зупинити
POST /api/v1/jobs/{id}/pause — призупинити
DELETE /api/v1/jobs/{id} — видалити

Питання на якому я фокусуюся в прикладі зі статті — чи має сенс ці три окремих методи (start/stop/pause) ховати за PUT/PATCH чи ні.

Те що далі може бути прорва роботи то інша тема, я лише про ту частину яка формує зовнішній інтерфейс.

Про те і мова, що зовнішній інтерфейс має бути якомога зрозумілішим і легким в розширенні.

Але ж в черзі у вас задачі будуть мати цілком конретні статуси щоб ваш код який їх виконує завжди знав на чому він зупинився і яка частина системи повинна далі з ним працювати.

Будуть мати статус, і це буде read-only властивість. Ми не просто встановлюємо статус «Running», ми наказуємо системі «Запустити процес». Згідно загальній логіці, статус «Running» — це лише наслідок успішного виконання цієї команди, а не вхідний параметр.

Формально я не можу сказати чи один підхід кращий за інший, але якщо ми додаємо якісь дії в path частину url це певною мірою розходиться з тим до чого люди звикли.

Про це і стаття, що «люди звикли», хоча на практиці ця звичка часто не несе практичної користі.

Та все що завгодно можна додати в body.

Коли ви робите PUT на якийсь ресурс, вказуючи перелік властивостей, то скоріш за все маєте на увазі, що у цього ресурсу такі властивості є, а не просто що у дії «оновити статус» є додаткові параметри. В найпростішому випадку можна було б сказати — «оскільки завдання можна зупинити один раз, то нормально мати властивість з причиною зупинки», і тоді ваш приклад був би валідним. Але, наприклад, завдання можна поставити на паузу багато разів, і бажано зберігати історію причин кожної зупинки. В цьому випадку ваш приклад не дуже підійде — бо у ресурсу є історія, в якій буде декілька переходів стану, і деякі з них — з причиною від користувача. Тобто властивості «причина» на самому ресурсі завдання не буде, а додавати її штучно в пейлоад для PUT — ще сильніше заплутує інтерфейс.

Є дії які можна зробити з сутністю, а є властивості. Для зміни властивостей нормально мати загальний метод на всіх, але для окремих дій краще виділяти окремі методи, бо у них, окрім всього іншого, можуть бути параметри і з плином часу їх список може розширюватися. Заміна явної дії оновленням властивості зменшує гнучкість і прозорість АПІ.

Підписатись на коментарі