Кожен туторіал вчить одному й тому самому REST API: п’ять роутів, happy path, JSON на вході, JSON на виході, готово за вечір. Потім API зустрічається з реальністю — мобільний клієнт, який три тижні не може оновитися, партнерська інтеграція, написана поверх одруківки, яку тепер не можна виправити, security-рев’ю, баг пагінації, який нікого не списав двічі, але всім усе показав двічі, — і ви виявляєте, що туторіал покрив легкі 20%. Про решту 80% і написано цей пост.
Це рішення, які мені доводилося захищати (або про які шкодувати) за роки production-API, — у тому порядку, в якому вони зазвичай кусають. І не випадково це саме ті теми, які промацує system design співбесіда, коли інтерв’юер каже: «а тепер спроєктуйте для цього API».
Як версіонувати API, який не можна ламати?
Версіонуйте з першого дня, в URL, і ставтеся до кожного опублікованого поля як до контракту, якого дотримуватиметеся роками. У момент, коли один консьюмер, якого ви не контролюєте, викочує код поверх вашого API, «потім почистимо» перетворюється на фікцію: перейменувати поле — breaking change, видалити поле — breaking change, змінити тип — це інцидент із вашим прізвищем. /v1/ у шляху негламурний і трохи неелегантний — і це підхід, у якого нудні режими відмови, а це найвищий комплімент у дизайні API.
Перш ніж іти рішення за рішенням, ось увесь пост однією таблицею — туторіальна відповідь, відповідь, що переживає п’ять років консьюмерів, і те, у що реально обходиться зрізаний кут, коли надходить рахунок:
| Рішення | Туторіальна відповідь | Відповідь, що виживає | У що обходиться зрізаний кут |
|---|---|---|---|
| Версіонування | Додавайте і перейменовуйте поля вільно | /v1/ з першого дня; після публікації — лише адитивні зміни | Кожне перейменування — інцидент усередині чужого застосунку |
| Auth за замовчуванням | Додайте auth на ті endpoint-и, яким він потрібен | Deny by default; публічні роути позначені явно | Той єдиний endpoint, який хтось забув, — краулер знайде його раніше за вас |
| Авторизація | Валідний токен означає, що викликачеві можна | Перевіряйте володіння на кожному читанні ресурсу | Дані вашого клієнта, віддані іншому клієнтові |
| Помилки | HTTP-статус плюс повідомлення прозою | Машинний код, людське повідомлення, correlation ID | Інтегратори гадають; їхній баг-репорт стає вашим дзвінком о 3-й ночі |
| Пагінація | ?page=3 | Непрозорий cursor зі змістом «продовжуй після цього рядка» | Елементи дублюються і зникають під записами; глибокі сторінки сканують і викидають |
| Аудиторія документації | Люди, які її погортають | Найбуквальніший консьюмер, який у вас коли-небудь буде | Агент галюцинує параметр, який ви лишили розпливчастим |
Справжня дисципліна не в схемі URL, а у звичці до адитивних змін. Нова можливість? Нове опціональне поле, новий endpoint — і ніколи не змінений сенс наявного. Команди, які засвоїли правило «опубліковано — значить назавжди», проєктують поля уважніше до публікації, і в цьому й полягає реальна користь: дисципліна версіонування — здебільшого forcing function, що змушує подумати двічі.
Як виглядає auth, що «fails closed»?
Кожен endpoint вимагає автентифікації, якщо він явно й свідомо не позначений як публічний — патерн deny-by-default middleware, де забута конфігурація роуту означає, що він закритий, а не відкритий. Повторювана катастрофа індустрії в API — не зламана криптографія, а endpoint, який хтось забув захистити і який знайшов краулер. Fail-closed дизайн робить таку помилку структурно неможливою: лінивий шлях і безпечний шлях — той самий шлях.
Авторизація заслуговує на ту саму параною рівнем глибше: автентифікація каже, хто ви, авторизація — що ваше, і зазор між ними — класичний злам. GET /orders/12345 з валідним токеном все одно зобов’язаний перевірити, що замовлення 12345 належить користувачеві цього токена — баг відсутньої перевірки володіння (IDOR мовою безпеки) залишається найпоширенішою серйозною вразливістю API на код-рев’ю, і згенерований код відтворює його з ентузіазмом, бо happy-path-версія виглядає ідентично.
Чому відповіді про помилки заслуговують часу на дизайн?
Бо ваші помилки — теж API, той самий, який о 3-й ночі споживає втомлений інтегратор, вирішуючи, чий це баг — його чи ваш. Живуча відповідь про помилку несе три речі: машиночитний код (insufficient_funds, а не просто HTTP 400), людиночитне повідомлення з іменем проблемного поля і correlation ID, за яким ваші логи та його баг-репорт знаходять одне одного. Самого HTTP-статусу замало: 400, що означає п’ять різних речей, змушує кожного консьюмера парсити вашу прозу.
Передбачуваність — фіча. Сюрприз — витрата. API, які називають «приємними в інтеграції», рідко бувають хитрими — вони роблять одне й те саме всюди.
Консистентність важливіша за багатство. Один error envelope, ідентична форма на кожному endpoint, задокументовано один раз. API, які описують як «приємні в інтеграції», рідко роблять щось хитре — вони роблять одне й те саме передбачуване всюди, тому обробка помилок в інтегратора — одна функція, а не по одній на endpoint. Передбачуваність — фіча; сюрприз — витрата, той самий урок, який durability викладає на рівні архітектури.
Що не так із пагінацією за номерами сторінок?
Offset-пагінація (?page=3) бреше за конкурентних записів: рядки, вставлені або видалені, поки консьюмер іде сторінками, змушують елементи з’являтися двічі або зникати, а глибокі offset-и карають базу повним scan-and-discard. Вона виживає в туторіалах, бо тривіальна і виглядає правильно в демо зі статичними даними. Cursor-пагінація — непрозорий токен зі змістом «продовжуй після цього рядка» — залишається коректною під записами і швидкою на глибині, ціною втрати «перейти на сторінку 7», яка більшості реальних консьюмерів ніколи не була потрібна.
Переносне правило: пагінуйте за позицією в упорядкуванні, а не за лічбою від початку, щоразу, коли дані змінюються під читачами. Це маленьке дизайн-рішення з довгим хвостом: прикручувати cursor-и до вже відвантаженого offset-API — саме та міграція опублікованого контракту, про яку попереджала секція про версіонування.
Чи змінюється щось, коли консьюмер — AI-агент?
Принципи виживають; допуски жорсткішають. Агенти споживають API через tool definitions, і все перелічене вище стає машинно-орієнтованим: розпливчасте повідомлення про помилку, повз яке людина знизала б плечима, відправляє агента в retry-цикл, неконсистентний envelope ламає його парсинг, недодокументований параметр — галюцинується. Проєктувати API, якими агенти можуть користуватися надійно, — точні schema, вичерпні коди помилок, чесні описи — те саме ремесло, що й хороший REST-дизайн, тільки без знижки на недбалість. У цьому теза пояснення MCP: інтерфейси інструментів — це дизайн API, другий раунд, з менш поблажливим консьюмером.
Ось чому шлях фундаментальними курсами досі проходить через побудову реальних REST-сервісів — Node.js і production-API заради самого ремесла, на поверсі backend-архітектури, де вирішується, що сервіс, а що handler, з TypeScript як дисципліною типізації, яка робить контракти примусовими, а не декларативними. У кожної з цих навичок тепер є другий замовник: агент, що викликає ваш API через schema, — так само буквальний, як тайпчекер, і помітно дорожчий, коли збитий з пантелику.
Чек-лист виживання
Версія в URL з першого дня; після публікації — лише адитивні зміни. Deny-by-default auth; перевірка володіння на кожному читанні ресурсу. Один error envelope усюди: машинний код, людське повідомлення, correlation ID. Cursor-и, а не offset-и, всюди, де дані рухаються. Документуйте для найбуквальнішого консьюмера, який у вас коли-небудь буде, — бо з цього року він у вас є. Ніщо з цього не гламурне. Як і міст, що не падає.