РозробкаБудь-який рівень

Дизайн API

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

Описується через
OpenAPI
Формат помилок
RFC 9457
Найкраща зміна версії
Та, якої не сталося

Контракт і реалізація

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

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

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

Ресурси, методи, коди статусу

Конвенція, якої дотримується більшість HTTP-API, — називати речі іменниками й казати методом, що з ними роблять. /orders/42 — це річ; GET, PUT і DELETE — те, що з нею роблять. Альтернатива, яка підповзає, — дієслова в шляху: /getOrder, /createOrderV2, /cancelOrderReally. Вона працює — і відмовляється від усіх конвенцій, які вже розуміють клієнтська бібліотека, кеш і проксі.

Дві властивості методів варто знати точно, бо від них залежать ретраї. Безпечний метод нічого не змінює — GET і HEAD, — тож клієнт, проксі чи краулер можуть викликати його вільно. Ідемпотентний метод можна викликати повторно з тим самим ефектом, що й один раз: PUT і DELETE такі, POST — ні. Саме тому мережевий збій на POST справді неоднозначний і саме тому всьому, що створює чи списує кошти, потрібен ключ ідемпотентності.

Коди статусу — та частина, яку всі знають наполовину. Сенс несуть класи: 2xx спрацювало, 3xx шукайте деінде, 4xx помилився викликач і повторення без змін не допоможе, 5xx помилилися ми й ретрай може спрацювати. Правильний клас важливіший за вибір між двома правдоподібними кодами всередині нього, бо клієнти, балансувальники й бібліотеки ретраїв розгалужуються саме за класом.

Помилки й пагінація

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

Для цього є стандартна форма, тож вигадувати не треба: problem details за RFC 9457, з типом контенту application/problem+json, де type ідентифікує клас помилки, title дає короткий заголовок, є status, detail для конкретного випадку й instance. Її використання дозволяє клієнтським бібліотекам парсити ваші помилки без окремої гілки під ваш API — і знімає найнуднішу суперечку в будь-якому рев’ю API.

У пагінації одне рішення, і більшість API роблять його неправильно. Пагінація за зсувом — ?page=3&size=20 — легко будується й легко уявляється, і ламається саме за тих умов, які мають реальні дані: якщо рядок вставили, поки клієнт на третій сторінці, один елемент зсувається на четверту і його не побачать ніколи — або побачать двічі. Вона ще й сповільнюється з глибиною, бо база відраховує все, що пропускає.

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

Версіонування і як його потребувати менше

Версіонування зазвичай обговорюють як вибір механізму: сегмент шляху на кшталт /v2, заголовок, тип медіа. Ця суперечка варта хвилин десяти: ставте версію в шлях, бо її видно в логах, у браузері й у тікеті підтримки, — і рухайтеся далі. Справді важливе рішення — як часто вам взагалі потрібна нова версія.

Більшість змін її не потребують. Додати поле, додати ендпоінт, додати необов’язковий параметр, почати приймати значення, яке раніше відхиляли, — усе це адитивне, і клієнт, який ігнорує нерозпізнане, працює далі. Публікація цього очікування — половина роботи: заявіть, що клієнти мають ігнорувати невідомі поля, і додавання полів перестане бути ламкою зміною за домовленістю, а не за щастям.

ЗмінаЛамка?Чому
Додати необов’язкове поле у відповідьНіТолерантні клієнти його ігнорують
Додати обов’язкове поле в запитТакКожен наявний виклик тепер падає
Перейменувати полеТакЦе видалення й додавання водночас
Посилити правило валідаціїТакЗапити, що працювали, тепер ні
Додати нове значення перелікуЗазвичай такКлієнти роблять на ньому вичерпний switch
Змінити типове значення поляТакПоведінка змінюється без зміни виклику

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

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

REST, GraphQL, gRPC

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

СтильПасуєСилаЦіна
REST через HTTPПублічні й партнерські APIКешується, дебажиться, зрозумілий усімНадлишкові й недостатні вибірки
GraphQLБагато клієнтів із різними потребамиКлієнт просить рівно те, що йому требаКешування, ліміти й вартість запитів
gRPCМіж сервісами, внутрішнєШвидко, типізовано, згенеровані клієнти, стрімиНе читається в браузері чи curl

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

Як це виявляється в реальній поставці

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

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

Де він вироджується

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

Коли це застосовувати

Застосовуйте, коли

  • Споживачі поза вашим деплоєм — інша команда, партнер, публіка.
  • Інтерфейс переживе реалізацію під ним — а це звичайна ситуація.
  • Ви можете написати специфікацію першою й отримати рев’ю до початку розробки.
  • Контрактні тести можуть іти в CI, тож на питання «чи це ламке?» відповідає збірка.

Уникайте, коли

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

Було корисно?

Поділіться з тим, хто працює над тією ж задачею.