Дизайн API
Дизайн API — це робота над поверхнею, на якій будують інші. Корисна рамка — одна лінія: над нею контракт, на який спираються споживачі і який ви не змінюєте самотужки; під нею реалізація, яку можна переписати у вівторок. Більшість того, що робить API болісним у житті, — це рішення, яке опинилося не на тому боці цієї лінії.
- Описується через
- OpenAPI
- Формат помилок
- RFC 9457
- Найкраща зміна версії
- Та, якої не сталося
Контракт і реалізація
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, тож на питання «чи це ламке?» відповідає збірка.
Уникайте, коли
- Два модулі в одній одиниці деплою — контрактом є сигнатура функції, і її перевіряє компілятор.
- Єдиний споживач, який деплоїться разом із вами, — можна змінити обидва одразу, і церемонія нічого не дає.
- Форма ще змінюється щотижня — стабілізуйте предметну область, перш ніж обіцяти про неї.
- Версіонувати публічний інтерфейс замість того, щоб вирішити, що входить у контракт.
Було корисно?
Поділіться з тим, хто працює над тією ж задачею.