Дизайн API
Дизайн API — це робота над тим, що інші програми можуть надсилати вашій, що вони отримають у відповідь і які з цих обіцянок ви триматимете роками. Усе, що споживач може спостерегти, стає частиною обіцянки, і забрати її самотужки ви не можете. Сторінка починається з шістнадцяти рішень, які ухвалює кожен HTTP-API, — що обрати за замовчуванням і чого коштує змінити кожне пізніше; далі — що входить у контракт, методи, коди статусу й помилки, поведінка під повторами й навантаженням, хто що може робити, зміна опублікованого API, п’ять задокументованих кейсів і де все це вироджується.
- Описується через
- OpenAPI 3.2 (вересень 2025)
- Формат помилок
- Problem details за RFC 9457
- Найчастіший злам
- Авторизація на рівні об’єкта (OWASP API1)
Шістнадцять рішень, які ухвалює кожен HTTP-API, — що обрати за замовчуванням і чого коштує змінити це пізніше
API проєктується незалежно від того, чи хтось сів його проєктувати. Кожен ендпоінт, що виходить у світ, відповідає на ті самі шістнадцять питань. Як він називає речі? Як виглядає збій? Що станеться, коли запит прийде двічі? Хто може читати який запис? Що зміна зробить із тими, хто його викликає? Якщо нічого не вирішувати, на кожне питання відповідає те, що фреймворк зробив за замовчуванням. Ця відповідь стає обіцянкою того дня, коли перший споживач її розпарсить. Ось шістнадцять рішень: що варто обрати за замовчуванням, чому і чого коштує передумати після релізу. Наступні розділи посилаються на рядки за номерами.
| # | Рішення | Що обрати за замовчуванням | Чому | Змінити пізніше |
|---|---|---|---|---|
| 1 | Назви й методи | Іменники в шляху, а дію каже метод: GET /orders/42, POST /orders | Кеші, проксі, клієнтські бібліотеки й логи вже розуміють методи; дієслова в шляху (/getOrder) це відкидають | Ламко: кожен викликач має шлях у своєму коді |
| 2 | Коди статусу | Спершу правильний клас: 2xx виконано, 4xx помилка викликача, 5xx ваша | Політики повторів, балансувальники й алерти розгалужуються за класом і тіла не читають | Ламко: клієнти розгалужуються за кодом |
| 3 | Помилки | Problem details за RFC 9457: стабільний type для програм, detail для людей | Програмі потрібен токен, за яким розгалужуватися; людині о другій ночі — речення | Ламко: клієнти вже парсять стару форму |
| 4 | Ідентифікатори | Непрозорі рядки, які неможливо вгадати, — не послідовні цілі числа | Послідовні id запрошують до перебору й видають ваші обсяги; 64-бітні цілі втрачають точність у JavaScript | Ламко: змінюється тип кожного поля з id |
| 5 | Формати даних | Час у RFC 3339 зі зсувом; гроші — ціле число в мінімальних одиницях плюс валюта; один стиль назв | Місцевий час без зсуву й сума з плаваючою комою помилкові так, що жоден тест цього не ловить | Ламко, поле за полем |
| 6 | Запит, надісланий двічі | PUT і DELETE ідемпотентні за задумом; заголовок Idempotency-Key на кожному POST, що створює чи списує гроші | Після таймауту викликач не знає, чи дія сталася; повтор без ключа виконує її двічі | Адитивно: необов’язковий заголовок, але кожен клієнт має почати його надсилати |
| 7 | Списки | Курсорна пагінація з обмеженим limit і стабільним порядком сортування | Сторінки за зсувом пропускають і повторюють рядки, поки дані змінюються, і сповільнюються з глибиною | Ламко: змінюються параметри пагінації |
| 8 | Ліміти | Ліміти частоти на клієнта з першого дня; 429 з Retry-After | Без них один баговий скрипт стає аварією для всіх | Ламко на практиці: хтось уже покладається на те, що їх немає |
| 9 | Повільна робота | 202 Accepted з посиланням на операцію, яку викликач може опитувати, або вебхук | Запит, відкритий хвилинами, помирає на першому таймауті проксі | Ламко: синхронна відповідь стає асинхронною |
| 10 | Паралельні зміни | ETag на кожному ресурсі; If-Match на оновленнях, 412, коли версія застаріла | Інакше два клієнти, що редагують той самий запис, мовчки перезаписують одне одного | Адитивно: необов’язкові заголовки |
| 11 | Автентифікація | Стандартна схема — OAuth 2.0 або API-ключі — у заголовку Authorization, на кожному маршруті, без винятків | Облікові дані в URL потрапляють у логи й історію браузера; нападникові досить одного відкритого маршруту | Ламко для маршрутів, які були відкриті |
| 12 | Авторизація для кожного об’єкта | Кожен запит, що називає запис, перевіряє, чи цей викликач має право на цей запис | Перевірка лише того, що викликач увійшов, — найпоширеніший спосіб, яким API зливають дані (OWASP API1) | Не зміна контракту, а виправлення, яке ви винні вже сьогодні |
| 13 | Опис | Документ OpenAPI, написаний до коду, переглянутий споживачем і перевірений у CI | Написаний після коду, він документує випадковості як рішення | Дешево зараз; пізніше кожен ендпоінт доведеться описувати з його поведінки |
| 14 | Правила сумісності | Опубліковані: клієнти ігнорують невідомі поля, переліки можуть рости, нічого не прибирають без попередження | Додати поле безпечно лише тоді, коли клієнтам сказали ігнорувати невідоме | Безкоштовно в перший день; заднім числом не запровадити |
| 15 | Версії | Мажорна версія в шляху або датована версія на акаунт — і якомога менше версій | Кожна жива версія подвоює кожне виправлення й кожну розмову з підтримкою | Механізм дешево обрати й дорого змінити |
| 16 | Видалення | Оголошене вікно, заголовки Deprecation і Sunset і використання, виміряне по кожному клієнту, перш ніж щось вимикати | Оголошення на вебсторінці доходить до тих, хто її читає; заголовок і метрика — до всіх інших | Це процедура зміни всіх інших рядків |
Що таке контракт — закон Гайрама, записаний опис і стиль, що його несе
API — це обіцянка про поведінку. Її дано людям, яких немає в кімнаті і яких не попросиш передеплоїтися за вашим графіком. У цій останній частині й полягає вся складність. Внутрішній код можна рефакторити щоразу, коли ви краще його зрозуміли. API — ні, бо хтось вісімнадцять місяців тому побудував на ньому інтеграцію із зарплатами й відтоді про вас не згадував.
Що ж саме ви пообіцяли? Менше, ніж думаєте, і більше, ніж хотіли. Гайрам Райт, тоді інженер Google, вклав другу половину в одне речення, яке його колега Тайтус Вінтерс назвав законом Гайрама: «За достатньої кількості користувачів API неважливо, що ви обіцяєте в контракті: на кожну спостережувану поведінку вашої системи хтось спиратиметься». Спостережуване — це все, що бачить викликач. Порядок елементів у масиві. Те, що id випадково зростають на одиницю. Точне формулювання повідомлення про помилку, яке хтось порівнює регулярним виразом. Скільки триває виклик. Нічого з цього не документували — і все це когось зламає, коли зміниться.
Друга половина контракту — хто мусить бути поблажливим. Стара порада — принцип надійності Джона Постела: будьте консервативними в тому, що надсилаєте, і ліберальними в тому, що приймаєте. Половина цієї поради постаріла добре. Клієнт має бути толерантним читачем — це термін Мартіна Фаулера з 2011 року: читати лише потрібні поля й ігнорувати решту, щоб нове поле не могло його зламати. Друга половина — ні. Сервер, що тихо приймає некоректний вхід — дату в трьох форматах, поле з помилкою в назві, яке він угадує, — перетворює кожен такий здогад на функцію, на яку вже хтось спирається. RFCRequest for Comments — нумерований стандарт інтернетуНумерований документ, який публікує IETF — організація, що стандартизує інтернет-протоколи; HTTP, TLS і JSON визначено саме в RFC. Опублікований RFC ніколи не змінюється — виправлення виходить новим RFC з новим номером, тому «RFC 9110» називає один точний текст.Усі терміни 9413, опублікований архітектурною радою IETF у червні 2023 року, стверджує саме це: поблажливість дає помилкам стати протоколом. Тож будьте суворими там, де приймаєте, і толерантними лише там, де читаєте відповіді.
| Хто | Суворий чи толерантний | Що це означає на практиці |
|---|---|---|
| Сервер, що приймає запит | Суворий | Перевіряти за описом; відхиляти з чітким 400 чи 422; ніколи не вгадувати, що мав на увазі викликач |
| Сервер, що надсилає відповідь | Консервативний | Лише задокументовані поля, у задокументованих форматах і задокументованому порядку |
| Клієнт, що читає відповідь | Толерантний | Читати потрібне, ігнорувати невідомі поля, обробляти значення переліку, якого ще не бачили |
| Клієнт, що надсилає запит | Консервативний | Надсилати лише задокументовані поля; ніколи не покладатися на те, що сервер пробачить помилку |
Пишіть опис до коду
Практика, що змінює результат більше за будь-яке окреме правило, — рядок 13: спершу специфікація. Документ OpenAPI — це один файл, що описує кожен шлях, параметр, тіло запиту й відповідь у вигляді, який читають машини. Поточну версію, 3.2, оголосили 23 вересня 2025 року. Вона спирається на узгодження з JSONJavaScript Object Notation — текстовий формат данихТекстовий формат для передавання даних: пари «ключ — значення», списки, числа й рядки. Він народився в JavaScript, але сьогодні його читає будь-яка мова — і саме тому ним відповідає більшість API, а не чимось потужнішим.Усі терміни Schema з версії 3.1 і додає новий метод QUERY, потокові відповіді на кшталт server-sent events і вкладені теги для великих API. Написаний першим, документ можуть переглянути ті, хто його споживатиме, ще до того, як щось збудовано. Того ж дня з нього генерується мок-сервер, на якому вони почнуть будувати. Пізніше з нього генеруються клієнтські бібліотеки й валідація запитів, тож код не може від нього відійти.
Написаний потім, згенерований із коду, той самий документ фіксує те, що збудували, — включно з тим, на що ніхто не погодився б на рев’ю. Решту розриву закриває друга практика — контрактні тести. У контрактах від споживачів (consumer-driven contracts), які Ієн Робінсон описав 2006 року, кожен споживач публікує ту частину вашого API, якою справді користується, а ваша збірка ганяє ці очікування проти постачальника. Інструменти на кшталт Pact це автоматизують. У результаті питання «чи ця зміна ламка?» перестає бути судженням і стає результатом збірки. Найважливіше це для змін, що виглядають нешкідливими, — саме там і живуть дорогі помилки.
REST, GraphQL, gRPC — і події
Більша частина сторінки виходить із HTTP і JSON, бо такими є більшість API між організаціями. Інші стилі не кращі й не гірші. Вони роблять різні компроміси щодо того, хто визначає форму відповіді, і правильний залежить від того, хто викликає. Кожен рядок таблиці вгорі так чи інакше стосується і їх.
| Стиль | Пасує | Сила | Ціна | Його правило сумісності |
|---|---|---|---|---|
| HTTP + JSON (REST) | Публічні й партнерські API | Кешується, дебажиться через curl, зрозумілий кожному проксі | Надлишкові й недостатні вибірки; багато звернень заради одного екрана | Рядки 14–16 цієї сторінки |
| GraphQL | Кілька насичених клієнтів, яким потрібні різні зрізи одного графа | Клієнт просить рівно ті поля, що йому потрібні, одним запитом | Втрачається HTTP-кешування; вартість і глибину запитів треба обмежувати (рядок 8) | Без версій: поля додають, а старі позначають @deprecated |
| gRPC | Виклики між сервісами, які ви експлуатуєте | Швидко, типізовано, згенеровані клієнти, потоки в обидва боки | Не читається в браузері чи curl; потребує HTTP/2 від краю до краю | Ніколи не використовувати номер поля повторно; видалені позначати reserved |
| Події й вебхуки | Повідомити інших, що щось сталося | Викликач не опитує; виробники й споживачі розв’язані в часі | Доставляються щонайменше раз, інколи не в тому порядку; споживачі мусять бути ідемпотентними | Форма події — теж контракт; описуйте її, наприклад, в AsyncAPI |
На практиці схема така: HTTP на межі, де споживачі невідомі, а API має прожити роки. gRPC — між сервісами, які ви контролюєте й деплоїте разом. GraphQL пасує там, де кілька насичених клієнтів потребують різних зрізів тих самих даних, а альтернатива — двадцять індивідуальних ендпоінтів. Його специфікація у вересні 2025 року отримала першу нову редакцію за чотири роки. Змішувати стилі нормально: стиль — властивість однієї межі, а не всієї системи.
Методи, коди статусу й помилки — те, що читає кожен шар між вами й викликачем
Рядки 1–3 виглядають як питання стилю. Вони ним не є, бо запит проходить через програми, які нічого не знають про ваш API й діють лише на основі методу та коду статусу. Браузер попередньо завантажує посилання. Краулер ходить ними. Проксі кешує відповіді. Клієнтська бібліотека повторює одні збої й не повторює інших. Кожна з цих програм читає метод і код статусу, і жодна не читає вашої документації.
Дві властивості методу варто знати точно, бо від них залежать повтори. Безпечний метод нічого не змінює на сервері, тож викликати його може будь-хто. Ідемпотентний метод має однаковий ефект, виконали його раз чи п’ять. RFC 9110, чинне визначення семантики HTTP, призначає обидві властивості кожному методу окремо. У червні 2026 року IETF додав перший новий стандартний метод від часів PATCH у 2010-му.
| Метод | Безпечний | Ідемпотентний | Для чого | Пастка |
|---|---|---|---|---|
| GET | Так | Так | Читання | GET, що змінює стан, запускають префетчери й краулери |
| POST | Ні | Ні | Створення й будь-яка дія, що не є заміною | Повтор створює другий екземпляр — звідси ключ ідемпотентності (рядок 6) |
| PUT | Ні | Так | Заміна цілого ресурсу за відомою адресою | Часткове тіло стирає поля, яких у ньому немає |
| PATCH | Ні | Не за визначенням | Зміна частини ресурсу | У JSON Merge Patch null видаляє поле, а відсутнє поле лишається як є |
| DELETE | Ні | Так | Видалення | Другий виклик повертає 404 або 204; годиться будь-що, якщо задокументовано |
| QUERY | Так | Так | Пошук, чий фільтр завеликий для URL (RFC 10008, червень 2026) | Новий: проксі й фреймворки ще наздоганяють, тож тримайте POST як запасний варіант |
Коди статусу — та частина, яку всі знають наполовину. Сенс несе клас: 2xx спрацювало, 3xx шукайте деінде, 4xx помилився викликач і повторення без змін не допоможе, 5xx помилилися ми й повтор може спрацювати. Правильний клас важливіший за вибір між двома правдоподібними кодами всередині нього. Майже будь-якому API вистачає дюжини кодів:
| Код | Що він каже викликачу | Коли використовувати |
|---|---|---|
| 200 / 201 / 204 | Виконано; створено (з Location); виконано, повертати нічого | Запит успішний |
| 202 | Прийнято, ще не завершено | Робота триває після відповіді (рядок 9) |
| 400 | Я не зміг прочитати ваш запит | Зламаний JSON, хибний тип, бракує обов’язкового заголовка |
| 401 | Я не знаю, хто ви | Облікових даних немає або вони недійсні — попри назву, «Unauthorized» означає «не автентифіковано» |
| 403 | Я знаю, хто ви, і вам цього не можна | Операція заборонена для цього викликача (рядок 12) |
| 404 | Тут нічого немає — для вас | Запису не існує або викликач не має дізнатися, що він існує |
| 409 | Поточний стан цього не дозволяє | Дублікат, замовлення вже відправлене, повтор, поки перша спроба ще виконується |
| 412 | Ваша копія застаріла | If-Match не збігся з поточним ETag (рядок 10) |
| 422 | Я прочитав, і це некоректно | Форма правильна, зміст хибний: дата кінця раніша за дату початку |
| 429 | Забагато запитів — зачекайте | Перевищено ліміт частоти; надсилайте Retry-After (рядок 8) |
| 500 / 503 | Наша провина; ми перевантажені чи тимчасово недоступні | Усе, чому викликач не міг запобігти; 503 з Retry-After, коли це мине |
Вибір між 403 і 404 — рішення безпеки, а не стилю. Припустімо, викликач просить рахунок, який існує, але належить іншому клієнтові. 403 повідомляє, що рахунок існує. З послідовними id це карта всіх ваших рахунків. GitHub документує той самий вибір для приватних репозиторіїв: запит без автентифікації отримує 404 Not Found, а не 403 Forbidden, щоб відповідь не підтверджувала, що репозиторій існує.
У відповіді з помилкою дві аудиторії з протилежними потребами. Програмі потрібен стабільний токен, за яким можна розгалужуватися. Людині, що дебажить о другій ночі, — речення. Є стандартна форма, що служить обом, тож вигадувати свою не треба: problem details за RFC 9457 із типом контенту application/problem+json. У липні 2023 року цей стандарт замінив RFC 7807 і зберіг його поля. type — це URI, що називає вид помилки й ніколи не змінюється. title — короткий опис цього виду. status повторює код. detail пояснює конкретний випадок. instance вказує на нього. Усе інше, що вам потрібно, іде в додаткові поля поруч.
Повтори, списки, ліміти й повільна робота — поведінка, на яку покладаються, не читаючи про неї
Запит, надісланий двічі
Рядок 6 починається з питання, з яким рано чи пізно стикається кожен клієнт. POST /payments відвалюється за таймаутом. Платіж відбувся? Клієнт не може цього знати. Запит міг загинути дорогою туди, а міг пройти — і тоді загинула відповідь дорогою назад. Повторити — ризикнути списати двічі. Не повторювати — ризикнути не списати зовсім. Мережевий збій на неідемпотентному запиті справді неоднозначний, і жодна обережність клієнта цієї неоднозначності не прибирає.
Сервер може її прибрати. Клієнт генерує унікальний ключ — випадковий UUIDUniversally Unique Identifier — 128-бітний id, створений будь-де128-бітний ідентифікатор, який будь-яка машина може згенерувати, не питаючи центрального лічильника, і практично без шансу на збіг двох. Версія 4 випадкова — її не вгадати, але вставляння в базу розкидаються; версія 7 починається з мітки часу, тож нові id сортуються в порядку створення.Усі терміни — і надсилає його в заголовку Idempotency-Key разом із запитом, а потім знову з кожним повтором того самого запиту. Сервер зберігає ключ разом із результатом першого виконання. Коли той самий ключ приходить знову, сервер не виконує запит удруге. Він повертає збережений результат. Stripe, чий API зробив цей патерн звичним, документує деталі, які варто перейняти. Він зберігає статус і тіло першого виконання, хоч успішного, хоч ні, включно з помилками 500. Ключі можна прибирати через 24 години. Ключ, повторно використаний з іншими параметрами, — це помилка, бо це баг клієнта, а не повтор. І нічого не зберігається, якщо запит не пройшов валідацію або зіткнувся із запитом з тим самим ключем, який ще виконувався, — такі можна просто повторити.
Назва заголовка — фактичний стандарт, але ще не формальний. Чернетка IETF описує його з 2021 року. Її сьома редакція, жовтня 2025 року, пропонує 422 для ключа, повторно використаного з іншим тілом, і помилку конфлікту для повтору, що прийшов, поки оригінал ще виконується. Ця чернетка спливла у квітні 2026 року, так і не ставши RFC. Дотримуйтеся її поведінки все одно, бо клієнтські бібліотеки вже на неї розраховують. І пам’ятайте межу патерну: він робить безпечним повтор одного ендпоінта. Для ланцюга сервісів за цим ендпоінтом він нічого не робить — кожному з них потрібен власний захист, про це сторінка про мікросервіси.
Списки
У пагінації одне рішення, і більшість API ухвалюють його неправильно. Пагінація за зсувом — ?page=3&size=20 — легко будується й легко уявляється. Вона ламається саме за тих умов, які мають реальні дані. Вставте рядок, поки клієнт читає третю сторінку, і один елемент зсунеться на четверту: його побачать двічі або ніколи. Вона ще й сповільнюється з глибиною, бо база відраховує кожен рядок, який пропускає, — чому так, показує сторінка про SQL. Курсорна пагінація віддає клієнтові непрозорий токен зі змістом «продовжуй рівно звідси». Вона лишається коректною, поки дані змінюються, і швидкою на будь-якій глибині. Ціна — клієнт не може стрибнути на сьому сторінку. Для API, який споживають програми, цього ніхто й не хотів.
Курсори працюють завдяки трьом деталям. Сортуйте за чимось унікальним — часом створення плюс id як розв’язувачем нічиїх, — інакше два рядки з однаковою міткою часу непередбачувано розійдуться по сторінках. Кодуйте курсор так, щоб викликачі не могли зібрати його вручну, — це лишає вам свободу змінювати те, що всередині. І обмежте limit: викликач, що просить мільйон рядків однією сторінкою, — це аварія, що чекає свого часу, а це вже справа рядка 8. Остання пастка — загальна кількість. Підрахунок кожного відповідного рядка коштує базі повного проходу на кожному запиті, тож зробіть його необов’язковим, оцінковим або приберіть зовсім.
Ліміти
API без лімітів працює до першого бажного скрипта в циклі. Тоді один викликач з’їдає потужність, спільну для всіх. Ліміти частоти на клієнта з першого дня перетворюють це на 429 Too Many Requests для одного викликача з заголовком Retry-After, що каже, коли пробувати знову. Перший день важить. Щойно нічний імпорт якогось партнера почне залежати від десяти тисяч запитів на хвилину, запровадження ліміту його зламає, і він назве це ламкою зміною — і матиме рацію. Обмежуйте те, що дороге, а не лише те, що часте: розмір сторінки, розмір тіла, глибину й вартість запиту GraphQL і будь-який запит, що запускає щось за реальні гроші, як-от SMS. Є чернетка IETF для стандартних заголовків RateLimit, що кажуть викликачам, скільки в них лишилося. У жовтні 2026 року це досі чернетка, тож більшість API використовує власні заголовки X-RateLimit-*, і це нормально, якщо їх задокументовано.
Повільна робота й паралельні зміни
Деякі операції тривають хвилини: звіт, конвертація відео, масовий імпорт. Тримати HTTP-запит відкритим так довго — значить упасти на першому проксі чи балансувальнику з коротшим таймаутом, і клієнт не знатиме, чи робота триває. Відповідь рядка 9 — одразу повернути 202 Accepted з посиланням на ресурс операції. Клієнт його опитує або отримує вебхук, коли все завершилося. Операція має статус — в очікуванні, виконується, успішна, невдала — і, коли закінчено, посилання на результат. Настанови Google для API називають це довготривалою операцією й дають кожній однакову форму, тож клієнт, написаний для однієї, працює з усіма.
Рядок 10 стосується двох людей, що редагують той самий запис. Обидва читають версію 5, обидва її змінюють — і друге збереження мовчки стирає перше. HTTP має відповідь уже десятиліттями. Кожна відповідь несе ETag — токен поточної версії. Оновлення надсилає його назад у If-Match. Якщо запис відтоді змінився, сервер відмовляє з 412 Precondition Failed, і клієнт перезавантажує дані замість того, щоб перезаписати. Сервер, що наполягає на цьому, відповідає на оновлення без If-Match кодом 428 Precondition Required. Це те саме оптимістичне блокування, яке сторінка про SQL описує всередині бази, лише винесене до викликача.
Хто що може робити — десять ризиків API і той, що стоїть за більшістю зламів
Два слова часто вживають так, ніби вони означають те саме. Автентифікація відповідає на питання «хто ви?». Авторизація — на питання «чи можна вам це?». Рядок 11 — автентифікація, і тут головне — нічого не вигадувати. Використовуйте стандартну схему — токени OAuth 2.0 або API-ключі — у заголовку Authorization, а не в URL, де вони потрапляють у логи сервера, логи проксі й історію браузера. А тоді застосуйте її до кожного маршруту. Одного маршруту, лишеного відкритим, «бо він внутрішній», нападникові досить.
Рядок 12 — місце, де ламається авторизація, і ламається вона конкретним, повторюваним способом. Маршрут перевіряє, що викликач увійшов. Далі обробник читає id зі шляху — /invoices/9081 — і завантажує цей запис. Ніщо не перевіряє, що рахунок 9081 належить викликачу. Змініть число — і ви читаєте чужий рахунок. OWASP називає це порушеною авторизацією на рівні об’єкта (broken object level authorization) і ставить на перше місце серед ризиків API в обох редакціях свого списку. Фреймворки від цього не захищають, бо фреймворк знає, хто користувач, але не знає, чий рахунок 9081. Це знає лише ваш код.
API Security Top 10 від OWASP у редакції 2023 року — досі чинній — перелічує десять ризиків. Кожен із них є проєктним рішенням, перш ніж стати вразливістю, тож ось усі десять: як кожен виглядає і що йому запобігає.
| # | Ризик | Як виглядає | Що запобігає |
|---|---|---|---|
| API1 | Порушена авторизація на рівні об’єкта | GET /accounts/43 працює для власника акаунта 42 | Власник у кожному запиті до бази; тест на кожен ендпоінт (рядок 12) |
| API2 | Порушена автентифікація | Токени, що ніколи не спливають; скидання пароля без ліміту спроб | Стандартна схема, короткоживучі токени, ліміти на вхід і скидання (рядок 11) |
| API3 | Порушена авторизація на рівні властивостей | Відповідь містить is_admin, і PATCH може його встановити | Явні схеми того, що виходить і що може прийти; жодного масового присвоєння |
| API4 | Необмежене споживання ресурсів | ?limit=1000000; ендпоінт, що надсилає SMS без обмежень | Обмеження розміру сторінки, тіла й вартості; ліміти на клієнта (рядок 8) |
| API5 | Порушена авторизація на рівні функцій | Звичайний користувач викликає DELETE /admin/users/7, і це спрацьовує | Заборона за замовчуванням; перевірка ролі на кожній операції |
| API6 | Необмежений доступ до чутливих бізнес-потоків | Боти викуповують увесь товар на старті; пошук контактів використовують, щоб звірити мільйони номерів | Ліміти на бізнес-потік, а не лише на запит; виявлення автоматизації |
| API7 | Підробка запитів на боці сервера (SSRF) | Функція «завантаж цей URL» дістається внутрішніх адрес | Список дозволених зовнішніх адрес |
| API8 | Хибна конфігурація безпеки | Стектрейси у відповідях з помилками, надто вільний CORS, HTTP без шифрування | Захищені налаштування за замовчуванням, перевірені в CI |
| API9 | Неналежний облік API | Стара версія чи тестовий хост досі живі й не оновлені | Перелік кожного хоста й версії, кожен — із власником (рядок 16) |
| API10 | Небезпечне споживання чужих API | Відповіді партнера довіряють і передають далі без перевірки | Перевіряти те, що надсилають партнери, так само суворо, як те, що надсилають користувачі |
Читайте список як набір питань для рев’ю дизайну, а не як чекліст для команди безпеки. П’ять із десяти — API1, API3, API4, API5 і API6 — це рішення про форму самого API. Пентест знаходить їх після релізу, і тоді виправлення часто стає ламкою зміною. Два кейси нижче, Optus і Facebook, — це API9 і API6 у масштабі країни.
Зміна опублікованого API — сумісність, версії й видалення
Версіонування зазвичай обговорюють як вибір механізму. Важливіше інше рішення — як рідко вам узагалі потрібна нова версія. Більшість змін її не потребує. Додати поле, додати ендпоінт, додати необов’язковий параметр, почати приймати значення, яке раніше відхиляли, — усе це адитивне, і толерантний клієнт працює далі. Опублікувати рядок 14 — половина роботи. Заявіть, що клієнти мають ігнорувати невідомі поля, і додавання поля перестане бути ламкою зміною за домовленістю, а не за щастям. GitHub показує, як далеко це сягає: його REST API понад три роки жив на одній датованій версії, 2022-11-28, доки не з’явилася наступна, 2026-03-10.
| Зміна | Ламка? | Як зробити її безпечно |
|---|---|---|
| Додати поле у відповідь | Ні — якщо клієнтам сказали ігнорувати невідомі поля | Просто додати |
| Додати необов’язковий параметр запиту | Ні | Без нього поведінка лишається старою |
| Додати обов’язкове поле запиту | Так | Зробити необов’язковим зі значенням за замовчуванням або додати новий ендпоінт |
| Перейменувати поле | Так — це видалення й додавання водночас | Додати нову назву, повертати обидві, оголосити стару застарілою, прибрати після вікна |
| Змінити тип поля | Так | Нове поле з новим типом; класичний приклад — id_str у Twitter |
| Дозволити полю бути null | Так — клієнти, що ніколи не бачили null, на ньому впадуть | Нове поле або задокументоване значення замість null |
| Посилити валідацію | Так — запити, що працювали, тепер падають | Спершу логувати чи попереджати; вимагати в наступній версії |
| Додати значення переліку | Зазвичай так | Сказати з першого дня, що значення можуть додаватися і що клієнтові робити з невідомим |
| Змінити значення за замовчуванням | Так — поведінка змінюється без жодної зміни у виклику | Дати нову поведінку через явний параметр |
Змінити type чи код статусу помилки | Так | Вважати помилки частиною контракту відповіді (рядок 3) |
| Прибрати ендпоінт чи поле | Так | Процедура видалення: рядок 16, нижче |
Рядок про перелік ловить людей, і його варто засвоїти. У більшості типізованих мов клієнт обробляє перелік вичерпним switch: три випадки, скомпільовано, протестовано. А тоді ви додаєте четвертий статус, і його код зустрічає значення, для якого немає гілки. Залежно від мови це падіння, тихе значення за замовчуванням або помилка десеріалізації, що відкидає всю відповідь. Якщо набір ростиме — а статуси замовлень, способи оплати й типи подій ростуть завжди, — скажіть це в документації з першого дня й поясніть клієнтам, що означає невідоме значення.
Коли ламка зміна справді потрібна, є чотири поширені механізми. Жоден не безкоштовний; кожен переносить ціну в інше місце.
| Механізм | Приклад | Сила | Ціна |
|---|---|---|---|
| Мажорна версія в шляху | /v2/orders | Видно в логах, браузерах і тікетах підтримки | Ціле нове дерево заради, можливо, однієї зміни; v1 живе роками |
| Датована версія на акаунт чи запит | Stripe-Version: 2024-09-30.acacia, X-GitHub-Api-Version: 2026-03-10 | Дрібні кроки; викликачі оновлюються, коли самі вирішать | Постачальник утримує перетворення для кожної минулої зміни |
| Тип медіа | Accept: application/vnd.example.v2+json | Точно, для кожного ресурсу | Не видно в браузері й у більшості логів |
| Без версій — лише адитивні зміни | Поля GraphQL з позначкою @deprecated | Не треба нічого обирати | Працює, доки зміну можна зробити адитивною |
Який механізм не оберіть, ціна не в тому, щоб створити версію. Вона в тому, щоб утримувати дві. Кожне виправлення доводиться застосовувати двічі. Кожна розмова з підтримкою починається зі з’ясування, на якій версії викликач. А стара версія ніколи не порожніє сама. Тому рядок 16 — це процедура, а не дата.
Як щось прибрати
Видалення має дійти до викликачів, які ніколи не читали вашого журналу змін, а це означає — повідомити їх у самій відповіді. Для цього є два стандартні заголовки. Deprecation, RFC 9745 з березня 2025 року, несе дату, від якої щось вважається застарілим, і посилання на пояснення. Sunset, RFC 8594 з травня 2019 року, несе дату, після якої це перестане працювати. Клієнтські бібліотеки й шлюзи API можуть перетворювати обидва на попередження у власних логах викликача. Тим часом ви вимірюєте, хто досі викликає старе, — по кожному клієнту, щотижня. Без цього числа неможливо відповісти на єдине важливе в день видалення питання: хто зламається?
Останні кілька викликачів — ті, до кого ніхто не може достукатися: інтеграція, чий автор звільнився, скрипт на сервері, про який ніхто не пам’ятає. Брауноут (brownout) знаходить їх, поки зміну ще можна скасувати. Вимкніть стару поведінку на кілька запланованих годин, а тоді ввімкніть знову. У них спрацюють алерти, і вони прийдуть до вас. GitHub зробив саме так перед тим, як прибрати автентифікацію паролем зі свого API. Він провів брауноути 30 вересня та 28 жовтня 2020 року, по два тригодинні вікна на день, а остаточне вимкнення відбулося 13 листопада 2020 року. Кожен із цих кроків має ще одну передумову: ви знаєте, хто ваші викликачі. Це знову рядок 16 — і API9 зі списку OWASP.
П’ять кейсів і те, чого в кожному бракувало
Кожен кейс нижче — це один рядок таблиці вгорі, що розігрався публічно, з контролем, якого бракувало або який спрацював.
| Кейс | Що сталося | Що це показує |
|---|---|---|
| Twitter, 2010 | Id твітів перейшли 53 біти — найбільше ціле, яке JavaScript зберігає точно; рядкові копії кожного id додали за місяць до цього | Id — це рядок, навіть коли він схожий на число (рядок 4) |
| Stripe, 2011–2024 | Кожен акаунт закріплено на версії його першого запиту; близько 100 ламких змін за шість років поглинули перетворення | Версіонування — ціна, яку платить постачальник, щоб не платили викликачі (рядок 15) |
| Kubernetes, 2019–2021 | Старі версії API перестали обслуговувати через два роки після виходу замін — і кластери все одно ламалися; попередження перенесли у відповіді | Попередження має приходити у відповіді й бути порахованим (рядок 16) |
| Optus, 2022 | Регулятор стверджує, що помилка в коді 2018 року на чотири роки лишила доступний з інтернету домен API без робочого контролю доступу | Кожен хост з обліку отримує ті самі перевірки (рядки 11–12, API9) |
| Facebook, 2018–2019 | Імпортер контактів використали, щоб зіставити номери телефонів із 533 мільйонами профілів; регулятор оштрафував Meta на 265 мільйонів євро | Обмежуйте бізнес-потік, а не лише частоту запитів (рядок 8, API6) |
Проблема Twitter почалася як успіх. 2010 року компанія замінила id твітів, які генерувала база, на Snowflake — сервіс, що видавав 64-бітні id у багатьох місцях одночасно. API повертав id як числа JSON. JavaScript зберігає кожне число як 64-бітне число з плаваючою комою, що точно тримає цілі лише до 53 бітів, тож клієнт на JavaScript, читаючи більший id, мовчки отримував інший. Змінити тип поля Twitter не міг, не зламавши кожного клієнта, що працював. Тож, як писали 18 жовтня 2010 року, з 22 жовтня він додав друге поле, id_str, з тим самим id у вигляді рядка й попередив, що id перейдуть 53 біти до 26 листопада. Клієнти мали п’ять тижнів, щоб перейти. Урок — рядок 4 і рядок таблиці вище про зміну типу: безпечна зміна була адитивною, і знадобилася вона лише тому, що перша версія зробила числом те, на чому ніхто й ніколи не рахував арифметики.
Stripe — протилежна історія: компанія, що вирішила, що версіонування — її витрата, а не витрата викликачів. Брандур Ліч описав цю систему в серпні 2017 року. Кожен акаунт закріплюється на найновішій версії під час першого запиту й лишається на ній, доки власник не оновиться. Версії — це дати. Усередині API будує кожну відповідь у поточній версії, а тоді веде її назад ланцюгом «модулів зміни версії» — по одному на кожну ламку зміну, — доки вона не відповідатиме версії, на якій закріплено викликача. На той час Stripe зробив близько сотні зворотно несумісних змін від 2011 року, і кожна стара версія досі працювала. 2024 року він додав поверх цього ритм: від 2024-09-30.acacia — щомісячні релізи без ламких змін і два іменовані мажорні релізи на рік. Урок — рядок 15. Найдешевша версія для викликача — та, про яку йому ніколи не треба думати, і хтось за це платить; Stripe вирішив платити сам.
Kubernetes показує, що попередження — не те саме, що міграція. Його API має формальні версії — v1beta1, v1 — і опубліковану політику застарівання. У липні 2019 року проєкт оголосив, що реліз 1.16 перестане обслуговувати старі бета-версії базових ресурсів, як-от Deployment і DaemonSet. Їхня стабільна заміна, apps/v1, була доступна з релізу 1.9 — на два роки раніше. Кластери все одно ламалися, бо ті, хто запускав старі маніфести й інструменти, так і не прочитали нотаток до релізу з попередженням. Виправлення прийшло в релізі 1.19 і перенесло попередження в саму відповідь. Кожен запит до застарілого API тепер отримує заголовок із попередженням, який kubectl друкує в терміналі користувача. Метрика apiserver_requested_deprecated_apis каже операторові кластера, які застарілі API досі викликають і в якому релізі кожен приберуть. Урок — рядок 16: видалення доходить до людей через відповідь і метрику, а не через оголошення.
Де це вироджується
Дизайн API рідко ламається на одному рішенні. Він ламається на звичці, якої ніхто не помічає, доки на неї не сперся перший споживач. Майже кожен болісний API має кілька з цього:
200 OKіз помилкою в тілі, що ховає збій від кожного шару, який читає коди статусу (рядок 2).- Колонки бази, відкриті як поля відповіді, — і кожна зміна схеми стає ламкою зміною API.
- Текст помилки як єдиний машинний сигнал — і клієнти порівнюють ваш англійський текст (рядок 3).
- Послідовні цілочисельні id, що запрошують до перебору й ламають JavaScript на 53 бітах (рядок 4).
POST, що списує гроші, без ключа ідемпотентності — і кожен таймаут стає тікетом у підтримку (рядок 6).- Пагінація за зсувом на даних, що змінюються, яка тихо пропускає й повторює рядки (рядок 7).
- Маршрут, що перевіряє вхід, але не власника, — найпоширеніший злам з усіх (рядок 12).
- Специфікація, згенерована з коду постфактум, що фіксує випадковості як рішення (рядок 13).
- Нова версія заради зміни, яку можна було зробити адитивною, що подвоює супровід ні за що (рядок 15).
- Застарівання, оголошене лише на вебсторінці, — і стара версія тримає трафік ще три роки потому (рядок 16).
Чесна межа в тому, що добрий дизайн купує час, а не імунітет. Закон Гайрама гарантує, що за достатньої кількості викликачів хтось спиратиметься на поведінку, якої ви ніколи не обіцяли, і жодне рев’ю не впіймає того, чого викликач ще не зробив. Контрактні тести покривають лише тих споживачів, що публікують контракти, а ламається зазвичай інтеграція, чий автор про ваші тести й не чув. Тож мірило добре спроєктованого API — не те, що воно ніколи нікого не ламає. А те, що ламка зміна рідкісна, оголошена у відповіді, виміряна по кожному викликачу й скасовується за хвилини, коли вимірювання помилилося.
Практична перевірка займає пів дня. Попросіть запис іншого акаунта через кожен ендпоінт, що приймає id, і порахуйте ті, що відповіли. Надішліть той самий POST двічі після імітованого таймауту й подивіться, чи створилося дві речі. Погортайте список, одночасно вставляючи рядки, і перевірте, чи щось пропущено. З’ясуйте, чи існує десь перелік усіх, хто викликає API, — кожного хоста, кожної версії, кожного клієнта. І пошукайте в коді 200, що повертається разом із помилкою.
Коли це застосовувати
Застосовуйте, коли
- Споживачі поза вашим деплоєм — інша команда, партнер, публіка.
- Інтерфейс переживе реалізацію під ним — а це звичайна ситуація.
- Ви можете написати специфікацію першою й дати її на рев’ю споживачу ще до розробки.
- Контрактні тести можуть іти в CI/CDContinuous Integration / Continuous Delivery — автоматична збірка й доставкаКожен коміт автоматично складається і проганяється через тести (CI), а придатна збірка так само автоматично їде на сервери (CD). Сенс не в автоматизації як такій, а в тому, що поломку видно за хвилини, а не в день релізу.Докладніше в статті →Усі терміни, тож на питання «чи це ламке?» відповідає збірка.
Уникайте, коли
- Два модулі в одній одиниці деплою — контрактом є сигнатура функції, і її перевіряє компілятор.
- Єдиний споживач, який деплоїться разом із вами, — можна змінити обидва одразу, і церемонія нічого не дає.
- Форма ще змінюється щотижня — стабілізуйте предметну область, перш ніж обіцяти щось про неї.
- Версіонувати публічний інтерфейс замість того, щоб вирішити, що входить у контракт.
Джерела й метод
На чому стоїть ця стаття
- 28 джерел
- 2 задокументовані збої
- Перевірено 13 жовтня 2026 р.
Практики й походження термінів
шістнадцять рішень: методи, помилки, повтори й правила змін- СтандартRFC 9110 — HTTP Semantics (відкривається в новій вкладці)IETF · 06.2022
- СтандартRFC 10008 — The HTTP QUERY Method (відкривається в новій вкладці)IETF · 06.2026
- СтандартRFC 9457 — Problem Details for HTTP APIs (відкривається в новій вкладці)IETF · 07.2023
- СтандартRFC 6585 — Additional HTTP Status Codes (відкривається в новій вкладці)IETF · 04.2012
- СтандартRFC 7396 — JSON Merge Patch (відкривається в новій вкладці)IETF
- СтандартRFC 9745 — The Deprecation HTTP Response Header Field (відкривається в новій вкладці)IETF · 03.2025
- СтандартRFC 8594 — The Sunset HTTP Header Field (відкривається в новій вкладці)IETF · 05.2019
- СтандартRFC 9413 — Maintaining Robust Protocols (відкривається в новій вкладці)IETF · 06.2023
- СтандартOpenAPI Specification v3.2.0 (відкривається в новій вкладці)OpenAPI Initiative · 09.2025
- СтандартGraphQL — September 2025 edition (відкривається в новій вкладці)GraphQL Foundation · 09.2025
- СтандартThe Idempotency-Key HTTP Header Field (draft-ietf-httpapi-idempotency-key-header-07) (відкривається в новій вкладці)IETF · 15.10.2025
- СтандартRateLimit header fields for HTTP (draft-ietf-httpapi-ratelimit-headers-11) (відкривається в новій вкладці)IETF
- ДокументаціяStripe API Reference — Idempotent requests (відкривається в новій вкладці)Stripe
- СтандартOWASP Top 10 API Security Risks – 2023 (відкривається в новій вкладці)OWASP · 2023
- СтаттяHyrum Wright — Hyrum’s Law (відкривається в новій вкладці)hyrumslaw.com
- СтаттяMartin Fowler — TolerantReader (відкривається в новій вкладці)martinfowler.com · 09.05.2011
- СтаттяIan Robinson — Consumer-Driven Contracts: A Service Evolution Pattern (відкривається в новій вкладці)martinfowler.com · 12.06.2006
- ДокументаціяGitHub REST API — API Versions (відкривається в новій вкладці)GitHub Docs
- ДокументаціяTroubleshooting the REST API (відкривається в новій вкладці)GitHub Docs
Збої, на яких вчиться стаття
Optus і Facebook у «П’ять кейсів і те, чого в кожному бракувало»Twitter, Stripe і Kubernetes
три інші кейси: зміна контракту, від якого залежать- СтаттяSnowflake: An update and some very important information (відкривається в новій вкладці)Twitter Development Talk · 10.2010
- СтаттяTwitter’s Snowflake Project To Update Tweet IDs Really Is More Like A Blizzard Now (відкривається в новій вкладці)TechCrunch · 18.10.2010
- СтаттяBrandur Leach — APIs as infrastructure: future-proofing Stripe with versioning (відкривається в новій вкладці)Stripe · 15.08.2017
- СтаттяIntroducing Stripe’s new API release process (відкривається в новій вкладці)Stripe · 2024
- СтаттяDeprecated APIs Removed In 1.16: Here’s What You Need To Know (відкривається в новій вкладці)Kubernetes Blog · 18.07.2019
- СтаттяJordan Liggitt — Warning: Helpful Warnings Ahead (відкривається в новій вкладці)Kubernetes Blog · 03.09.2020
- СтаттяKubernetes API and Feature Removals In 1.22: Here’s What You Need To Know (відкривається в новій вкладці)Kubernetes Blog · 14.07.2021
Дослідження з Claude. Пошук і звірку першоджерел зроблено з Claude (Anthropic). Відбір, текст і перевірку фактів — редакція Anatomy of IT Product.
Було корисно?
Поділіться з тим, хто працює над тією ж задачею.