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

Дизайн 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 — це обіцянка про поведінку. Її дано людям, яких немає в кімнаті і яких не попросиш передеплоїтися за вашим графіком. У цій останній частині й полягає вся складність. Внутрішній код можна рефакторити щоразу, коли ви краще його зрозуміли. API — ні, бо хтось вісімнадцять місяців тому побудував на ньому інтеграцію із зарплатами й відтоді про вас не згадував.

Що ж саме ви пообіцяли? Менше, ніж думаєте, і більше, ніж хотіли. Гайрам Райт, тоді інженер Google, вклав другу половину в одне речення, яке його колега Тайтус Вінтерс назвав законом Гайрама: «За достатньої кількості користувачів API неважливо, що ви обіцяєте в контракті: на кожну спостережувану поведінку вашої системи хтось спиратиметься». Спостережуване — це все, що бачить викликач. Порядок елементів у масиві. Те, що id випадково зростають на одиницю. Точне формулювання повідомлення про помилку, яке хтось порівнює регулярним виразом. Скільки триває виклик. Нічого з цього не документували — і все це когось зламає, коли зміниться.

Друга половина контракту — хто мусить бути поблажливим. Стара порада — принцип надійності Джона Постела: будьте консервативними в тому, що надсилаєте, і ліберальними в тому, що приймаєте. Половина цієї поради постаріла добре. Клієнт має бути толерантним читачем — це термін Мартіна Фаулера з 2011 року: читати лише потрібні поля й ігнорувати решту, щоб нове поле не могло його зламати. Друга половина — ні. Сервер, що тихо приймає некоректний вхід — дату в трьох форматах, поле з помилкою в назві, яке він угадує, — перетворює кожен такий здогад на функцію, на яку вже хтось спирається. 9413, опублікований архітектурною радою IETF у червні 2023 року, стверджує саме це: поблажливість дає помилкам стати протоколом. Тож будьте суворими там, де приймаєте, і толерантними лише там, де читаєте відповіді.

ХтоСуворий чи толерантнийЩо це означає на практиці
Сервер, що приймає запитСуворийПеревіряти за описом; відхиляти з чітким 400 чи 422; ніколи не вгадувати, що мав на увазі викликач
Сервер, що надсилає відповідьКонсервативнийЛише задокументовані поля, у задокументованих форматах і задокументованому порядку
Клієнт, що читає відповідьТолерантнийЧитати потрібне, ігнорувати невідомі поля, обробляти значення переліку, якого ще не бачили
Клієнт, що надсилає запитКонсервативнийНадсилати лише задокументовані поля; ніколи не покладатися на те, що сервер пробачить помилку

Пишіть опис до коду

Практика, що змінює результат більше за будь-яке окреме правило, — рядок 13: спершу специфікація. Документ OpenAPI — це один файл, що описує кожен шлях, параметр, тіло запиту й відповідь у вигляді, який читають машини. Поточну версію, 3.2, оголосили 23 вересня 2025 року. Вона спирається на узгодження з 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 відвалюється за таймаутом. Платіж відбувся? Клієнт не може цього знати. Запит міг загинути дорогою туди, а міг пройти — і тоді загинула відповідь дорогою назад. Повторити — ризикнути списати двічі. Не повторювати — ризикнути не списати зовсім. Мережевий збій на неідемпотентному запиті справді неоднозначний, і жодна обережність клієнта цієї неоднозначності не прибирає.

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

Таймаут на POST лишає клієнта без знання, чи відбувся платіж: запит міг загинути дорогою туди, а міг пройти — і тоді загубилася лише відповідь. Без ключа повтор — це новий запит, і кошти списуються двічі. З Idempotency-Key клієнт надсилає той самий випадковий ключ і з запитом, і з кожним повтором; сервер зберігає статус і тіло першого виконання під цим ключем і віддає їх будь-якому повтору замість того, щоб виконувати запит знову. Stripe зберігає результат, навіть коли це помилка, може прибирати ключі через 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. Це знає лише ваш код.

Автентифікація відповідає, хто викликач; авторизація — що йому можна: спершу чи можна викликати операцію взагалі, потім чи можна торкатися саме того запису, який називає запит. На перші два питання відповідають фреймворки, тож обидва пройдено. Третє — авторизація на рівні об’єкта, і відповісти на нього може лише код застосунку, бо лише застосунок знає, чий рахунок 9081. Без неї зміна числа в шляху дає прочитати чужі дані — це порушена авторизація на рівні об’єкта, перша в обох редакціях OWASP API Security Top 10. Якщо вписати власника в сам запит до бази, запис іншого акаунта просто не знаходиться, а 404 не видає, що він існує.

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 можуть перетворювати обидва на попередження у власних логах викликача. Тим часом ви вимірюєте, хто досі викликає старе, — по кожному клієнту, щотижня. Без цього числа неможливо відповісти на єдине важливе в день видалення питання: хто зламається?

Прибрати щось з опублікованого API — це процедура, а не дата. Застарівання з першого дня потрапляє в кожну відповідь — у заголовок Deprecation (RFC 9745) і заголовок Sunset (RFC 8594), а виклики старої версії рахують по кожному клієнту щотижня. Більшість викликачів переходять рано; довгий хвіст — ні, бо оголошення ніхто не читає: інтеграція, чий автор звільнився, забутий скрипт. Брауноут вимикає стару версію на кілька запланованих годин, щоб у цих викликачів спрацювали алерти, поки зміну ще можна скасувати; GitHub провів два такі вимкнення перед тим, як прибрати автентифікацію паролем у листопаді 2020 року. У день Sunset стара версія відповідає 410 Gone. Крива ілюстративна; форма — це процедура.

Останні кілька викликачів — ті, до кого ніхто не може достукатися: інтеграція, чий автор звільнився, скрипт на сервері, про який ніхто не пам’ятає. Брауноут (brownout) знаходить їх, поки зміну ще можна скасувати. Вимкніть стару поведінку на кілька запланованих годин, а тоді ввімкніть знову. У них спрацюють алерти, і вони прийдуть до вас. GitHub зробив саме так перед тим, як прибрати автентифікацію паролем зі свого API. Він провів брауноути 30 вересня та 28 жовтня 2020 року, по два тригодинні вікна на день, а остаточне вимкнення відбулося 13 листопада 2020 року. Кожен із цих кроків має ще одну передумову: ви знаєте, хто ваші викликачі. Це знову рядок 16 — і API9 зі списку OWASP.

П’ять кейсів і те, чого в кожному бракувало

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

КейсЩо сталосяЩо це показує
Twitter, 2010Id твітів перейшли 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, що повертається разом із помилкою.

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

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

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

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

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

Джерела й метод

На чому стоїть ця стаття

  • 28 джерел
  • 2 задокументовані збої
  • Перевірено 13 жовтня 2026 р.

Практики й походження термінів

шістнадцять рішень: методи, помилки, повтори й правила змін

Дослідження з Claude. Пошук і звірку першоджерел зроблено з Claude (Anthropic). Відбір, текст і перевірку фактів — редакція Anatomy of IT Product.

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

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