vyvchy
    Теми розділу

    03 · Веб і мережі для AQA

    REST API та формати даних

    Зміст

    Кнопка «Зберегти» — це не подія в базі даних, а HTTP-запит до ресурсу за конкретною адресою: метод каже, що зробити, тіло несе дані, повертає вердикт. Поки цей рівень невидимий, баги виглядають однаково — «щось не працює». Коли він видимий, ви за пів хвилини кажете, чиє це: 422 із поясненням у тілі — це валідація бекенда, 200 із порожнім масивом — це дані, а не верстка, а 415 — це ваш власний тест, який надіслав JSON туди, де чекали форму.

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

    Клієнт-сервер: рамка, у якій усе це живе

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

    Запит складається з методу, шляху, заголовків і тіла; відповідь — зі статус-коду, заголовків і тіла. Звична формула «один запит — одна відповідь» точна лише щодо фінальної відповіді: специфікація прямо «zero or more "interim" (non-final) responses» зі статусами класу 1xx, за якими йде рівно одна фінальна. Семантика при цьому не залежить від версії протоколу — вимоги сформульовані так, щоб повідомлення можна було ретранслювати між версіями без зміни змісту.

    СерверКлієнт — браузер, curl, автотестСерверКлієнт — браузер, curl, автотестнуль або більше проміжних 1xx, рівно одна фінальнаPOST /api/v1/orders + заголовки + тіло100 Continue (необовʼязково, проміжна)201 Created + заголовки + тілоСерверКлієнт — браузер, curl, автотестСерверКлієнт — браузер, curl, автотестнуль або більше проміжних 1xx, рівно одна фінальнаPOST /api/v1/orders + заголовки + тіло100 Continue (необовʼязково, проміжна)201 Created + заголовки + тіло

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

    REST — це набір обмежень, а не формат даних

    REST (Representational State Transfer) — не протокол і не стандарт, а архітектурний стиль для розподілених гіпермедійних систем; його описав Рой Філдінг (Roy Fielding) у докторській дисертації 2000 року. Стиль задає обмеження на взаємодію компонентів, і система, що їх дотримується, вважається RESTful.

    Обмежень шість:

    • client-server — розділення відповідальностей: інтерфейс окремо, зберігання даних окремо;
    • stateless — кожен запит містить усе потрібне для розуміння, стан сесії лишається на клієнті;
    • cache — відповідь має бути явно або неявно позначена як кешована чи ні, а кешовану клієнт має право перевикористати (плата — застарілих даних; докладніше в главі «Кешування»);
    • uniform interface — центральна риса, що відрізняє REST від інших мережевих стилів; складається з чотирьох підобмежень: ідентифікація ресурсів, маніпуляція ресурсами через представлення, самоописові повідомлення й гіпермедіа як рушій стану застосунку (HATEOAS);
    • layered system — компонент не бачить далі сусіднього шару;
    • code on demand — єдине необовʼязкове обмеження: сервер може віддати виконуваний код.

    Головне, що варто винести: жодне з обмежень не згадує JSON. Формат обміну до стилю не належить узагалі — REST задає, як влаштована взаємодія, а не в чому серіалізовані дані.

    Наша практика (не канон). Далі — те, як це виглядає в реальних командах; окремої специфікації під це немає. HATEOAS реалізують рідко, і більшість «REST API» його ігнорують, а самим терміном часто називають будь-який HTTP-API з JSON-відповідями. Тому на співбесіді безпечніше казати «HTTP-API у стилі REST» і не сперечатися про чистоту: практична цінність слова REST — у передбачуваності адрес, методів і самодостатніх запитів, а не в сертифікаті відповідності.

    Безстановість: кожен запит самодостатній

    HTTP визначено як безстановий (stateless) протокол: семантику кожного повідомлення можна зрозуміти ізольовано, а сервер не має права припускати, що два запити на одному зʼєднанні прийшли від того самого агента, якщо саме зʼєднання не захищене й не виділене цьому агентові. Те саме обмеження формулює REST: запит мусить нести всю інформацію, потрібну для його розуміння, і не може спиратися на збережений на сервері контекст.

    Мотив — масштабованість: якщо сервер не мусить памʼятати клієнтів, будь-який із десятка однакових серверів обробить будь-який запит. Другий наслідок — видимість: щоб визначити природу запиту, системі моніторингу (і вам у панелі Network) достатньо самого запиту, без відновлення історії.

    Практично це означає, що «памʼять» доводиться будувати поверх протоколу, і будують її двома способами. Кукі: сервер ставить її заголовком Set-Cookie, а агент користувача сам вирішує, чи повертати пари «імʼя-значення» у заголовку Cookie наступних запитів. Токен: специфікація задає форму запиту — credentials = "Bearer" 1*SP b64token у заголовку Authorization — і вимоги до сервера, а обовʼязків на клієнтський код не покладає. Для тесту звідси випливає найдешевший спосіб авторизації: отримати сесію одним API-викликом і підкласти її в контекст, а не проклацувати форму входу в кожному сценарії.

    Наша практика (не канон). Асиметрію двох механізмів джерела прямо не формулюють, але вона щоденна: кукі доїде сама, а токен поїде лише той, який поклав код застосунку. Тому «розлогінений тест» на кукі й на токені діагностується по-різному — деталі в главі «Кукі, сесії та сховище браузера».

    Ресурс і ендпоінт: чому це не синоніми

    Ресурс (resource) — ключова абстракція REST: ним може бути будь-що, чому можна дати назву — документ, зображення, послуга в часі («сьогоднішня погода в Лос-Анджелесі»), колекція інших ресурсів, невіртуальний обʼєкт на кшталт людини. Головне в означенні інше: ресурс — це концептуальне відображення на набір сутностей, а не сутність, що відповідає йому в конкретний момент. Вміст за тією самою адресою змінюється, а ресурс лишається тим самим.

    (endpoint) — конкретна адреса, за якою до ресурсу звертаються. Ресурс — «що», ендпоінт — «де». А діють над ресурсом через представлення (representation): послідовність байтів плюс метадані, що описують його поточний або бажаний стан. Тому «формат відповіді» і «ресурс» — теж різні речі: JSON у тілі є представленням, а не самим ресурсом.

    ідентифікується

    метод

    віддається як

    Ресурс: користувач 42

    Ендпоінт: /api/v1/users/42

    Операція: GET, PUT, DELETE

    Представлення: тіло + Content-Type

    ідентифікується

    метод

    віддається як

    Ресурс: користувач 42

    Ендпоінт: /api/v1/users/42

    Операція: GET, PUT, DELETE

    Представлення: тіло + Content-Type

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

    АдресаЩо це
    /api/v1/usersколекція користувачів
    /api/v1/users/42конкретний користувач
    /api/v1/users/42/ordersзамовлення користувача 42 (вкладений ресурс)
    /api/v1/orders?status=paidта сама колекція замовлень із фільтром

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

    Наша практика (не канон). Три речі нижче — конвенції індустрії, а не вимоги специфікацій. Дієслова в адресах (/getUser, /createOrder) вважають антипатерном, бо дію вже задає метод. Версію найчастіше вводять у шлях (/api/v1/users), рідше — заголовком чи query-параметром. Колекції віддають порціями двома способами: offset/limit дозволяє стрибнути на довільну сторінку, але «пливе», коли дані змінюються між запитами, а курсор — непрозорий маркер позиції — стабільніший, зате зазвичай ходить лише вперед-назад.

    Адреса: що з URL доходить до сервера

    Формально адреса — це послідовність компонентів: URI = scheme ":" hier-part [ "?" query ] [ "#" fragment ], де authority ділиться на userinfo, хост і порт. Для API важливі три наслідки.

    Перший: клієнт надсилає серверу дані з authority, шляху й запиту — фрагмент відокремлюється ще до звернення до ресурсу й обробляється виключно клієнтом. У логах API фрагмента немає ніколи. Другий: схема й хост нечутливі до регістру й нормалізуються в нижній, а шлях і запит вважаються чутливими, якщо схема не каже інакше, — звідси «404 на рівному місці» після того, як тест склеїв URL із даних. Третій: типові порти особливих схем у серіалізації опускають, тож https://api.example.com:443/v1 і https://api.example.com/v1 — та сама адреса.

    Внутрішньої структури рядка запиту специфікація не диктує взагалі: розбиття на пари ключ=значення, зʼєднані &, — домовленість, успадкована від HTML-форм. Тому «а що буде, якщо той самий параметр передати двічі» — питання до конкретного бекенда, а не до стандарту. Довжину зверху теж ніхто не обмежує, зате задано нижню межу підтримки: RECOMMENDED тримати URI щонайменше у 8000 октетів. Практичні межі ставлять вебсервери — в Apache це LimitRequestLine з дефолтом 8190 байтів, а nginx на завеликий стартовий рядок віддає 414 (Request-URI Too Large). Тіло при цьому має власний ліміт, на порядки більший, — звідси й правило «великі дані передаємо тілом, а не в адресі». Повний розбір — у главі «URL і кодування».

    Відсоткове кодування в шляху й рядку запиту

    Відсоткове кодування (percent-encoding) — механізм подання байта трійкою символів: % і дві шістнадцяткові цифри його значення. Кодують тоді, коли символ виходить за дозволений набір або вживається як роздільник компонента. Незарезервовані символи — латинські літери, цифри, дефіс, крапка, підкреслення й тильда — кодування не потребують.

    Механізм двокроковий, і саме тут ламається інтуїція: спершу текст перетворюється на байти (у вебі майже завжди UTF-8), і лише потім кожен байт окремо переписується як %XX. Пробіл 0x20 дає %20, а одна українська «і» (U+0456) — аж дві трійки, %D1%96. Набір символів, що підлягають кодуванню, залежить від компонента адреси, а самі шістнадцяткові цифри регістронезалежні: %2F і %2f кодують той самий байт.

    Головна пастка для API — дві різні угоди про пробіл. У режимі application/x-www-form-urlencoded пробіл кодується як +, а не %20; відповідно справжній плюс у цьому форматі записується %2B, тоді як у шляху + — це літеральний плюс. Ті самі граблі в коді тесту: encodeURIComponent дає для пробілу %20 і кодує &, URLSearchParams серіалізує пробіл як +, а encodeURI навмисно не чіпає роздільники URI (; / ? : @ & = + $ , #) — він призначений для вже сформованого URL цілком, а для окремого динамічного значення беруть encodeURIComponent. Практичний висновок один: порівнюйте декодовані значення, а не сирі рядки, інакше ?q=search+results і ?q=search%20results дадуть червоний асерт на однакових даних.

    Метод — це операція, а не частина адреси

    Метод каже, що зробити з ресурсом. GET запитує передачу поточного вибраного представлення; POST просить ресурс обробити передане представлення за власною семантикою; PUT створює або повністю замінює стан ресурсу; DELETE просить розірвати звʼязок ресурсу з його поточною функціональністю (це не обіцянка фізичного видалення рядка з бази). PATCH вносить часткові зміни, і різниця з PUT — у тому, чим є тіло: у PUT це модифікована версія ресурсу, у PATCH — набір інструкцій, як його змінити. Є ще HEAD — ідентичний GET, але сервер MUST NOT надсилати тіло, — і OPTIONS, який запитує опції взаємодії без дії над ресурсом; у браузері це насамперед CORS-preflight (див. «CORS і політика одного походження»).

    Дві наскрізні властивості методів прямо годують надійність тестів.

    Безпечний (safe) — той, чия визначена семантика фактично «лише читання»: клієнт не просить і не очікує зміни стану на сервері. Безпечні — GET, HEAD, OPTIONS, TRACE. Це не про захист даних і не про повну відсутність побічних ефектів: сервер може писати в лог доступу, важливо лише, що клієнт цього не просив і не відповідає за це. Мета поділу названа в специфікації прямо — щоб й працювали «without fear of causing harm».

    Ідемпотентний (idempotent) — той, у якого намірений ефект кількох однакових запитів дорівнює ефекту одного. Такими є PUT, DELETE і всі ; неідемпотентні — POST, PATCH і CONNECT. Ключова тонкість: стосується стану сервера, а не тіла відповіді. DELETE /users/42 двічі поспіль лишає систему в тому самому стані, хоч перший виклик поверне 204, а другий — 404. Для PATCH є пряма норма: «PATCH is neither safe nor idempotent», — але конкретний запит можна зробити ідемпотентним, і специфікація радить умовний запит (наприклад, зі строгим ETag в If-Match), щоб два одночасні патчі не затерли один одного.

    ОпераціяМетодБезпечнийІдемпотентний
    ЧитанняGETтактак
    СтворенняPOSTніні
    Повна замінаPUTнітак
    Часткова змінаPATCHніні
    ВидаленняDELETEнітак
    Метадані без тілаHEADтактак
    Опції взаємодіїOPTIONSтактак

    Практичний наслідок для тестів прямий: ідемпотентний запит можна безпечно повторити після чи моргання мережі, а наївний повтор POST створить другий обʼєкт — звідси «привиди» в базі, які потім валять сусідні тести. Кешованість — окрема вісь, яка з цими двома не збігається: за замовчуванням кешовані лише GET і HEAD. І останнє: HTTP описує наміри, а не гарантує поведінку реалізації. GET, який щось мутує, технічно можливий — і саме тому це баг контракту, а не особливість. Канонічний розбір методів і заголовків — у главі «HTTP: методи, структура, заголовки».

    Статус-код — частина контракту, а не «щось повернулося»

    Статус-код — тризначне число в діапазоні 100–599; клас задає перша цифра, дві останні класифікаційної ролі не мають. поруч із кодом специфікація називає необовʼязковою, а клієнту приписує (SHOULD) її ігнорувати — тому в тесті асертять число, а не рядок.

    КодЩо каже про контракт
    200 OKуспіх; конкретний зміст залежить від методу
    201 Createdстворено новий ресурс; за домовленістю з Location на його адресу
    204 No Contentтіла немає, заголовки значущі; парсити нічого
    400 Bad Requestпомилка клієнта, зокрема зламаний синтаксис
    401 Unauthorizedне автентифіковано; відповідь мусить нести WWW-Authenticate
    403 Forbiddenклієнт серверу відомий, але прав на ресурс немає
    404 Not Foundі невідомий шлях, і відсутній обʼєкт за валідним ендпоінтом
    405 Method Not Allowedметод відомий, але не підтримується цим ресурсом
    409 Conflictзапит конфліктує з поточним станом
    415 Unsupported Media Typeформат тіла не підтримується для цього ресурсу
    422 Unprocessable Contentформа коректна, семантика — ні
    429 Too Many Requestsперевищено рейт-ліміт; може нести Retry-After
    5xxсервер не впорався з очевидно валідним запитом

    Дві пари кодів дають половину негативних сценаріїв. 400 проти 422: «я не зрозумів, що ти написав» проти «я зрозумів, але так не можна». 401 проти 403: «не автентифіковано» проти «автентифіковано, але не дозволено». Окремо варто знати, що 429 визначає не основна специфікація HTTP, а окремий документ — RFC 6585, і що Retry-After несе або число секунд, або HTTP-дату. І ще: на відверто кривий ввід добре спроєктований сервер відповідає 400 чи 422, а не падає з 500, — тож 5xx майже завжди означає щонайменше одну проблему на бекенді. Повна таблиця з розборами — у главі «HTTP статус-коди».

    Наша практика (не канон). Реальний антипатерн, якого специфікації не описують: 200 OK із тілом {"error": "not found"}. Тест, що дивиться лише на статус, від такого зеленіє. Мінімальний чесний асерт складається з трьох частин — статус, Content-Type і структура тіла; повний чек-лист перевірок відповіді живе в розділі про API-тестування.

    Тіло повідомлення й типи вмісту

    Тіло (body) — необовʼязкова частина повідомлення після порожнього рядка. У запиті воно несе дані на сервер, у відповіді — запитані дані або пояснення . Що саме лежить у тілі, оголошує Content-Type, а скільки його — Content-Length; без них не знає ні формату, ні межі даних. Тіло в запиті за MDN мають лише PATCH, POST і PUT; у відповіді тіло є в більшості випадків, але не завжди — 204 No Content не несе його за означенням, тож парсити там нічого. Тіло в GET синтаксис не забороняє, але воно не має загальновизначеного значення й не може змінити зміст чи ціль запиту — тому складні «читальні» запити роблять через POST.

    Три формати тіла, які QA бачить щодня:

    • application/json — вкладені обʼєкти й масиви з типами; формат сучасних REST-API;
    • application/x-www-form-urlencoded — плоскі пари ключ=значення, зʼєднані &, зі спецсимволами у відсотковому кодуванні (пробіл — +); це те, що історично шле HTML-форма;
    • multipart/form-data — тіло, розбите на частини з власними міні-заголовками; частини розділяє рядок із двох дефісів плюс значення boundary з Content-Type, а завершальний роздільник має ще два дефіси в кінці. Значення boundary не має права трапитися всередині вмісту.

    Класична причина «у браузері працює, а в тесті — ні» саме тут: Content-Type не відповідає реальному тілу. Канонічна відповідь на це — 415 Unsupported Media Type, а не 400: специфікація прямо називає причиною «the request's indicated Content-Type or Content-Encoding». Сервер, що відповідає 400, дає менш точний код: 400 описує загальну помилку клієнта, а не відмову саме через медіа-тип, — і в баг-репорті це варто називати. Окремо про multipart: складати його руками легко зіпсувати на boundary чи заголовках частин, тому файл передають обʼєктом і дають бібліотеці зібрати тіло самій.

    // Playwright: тіло й Content-Type формує клієнт, а не ви руками
    await request.post('/api/v1/users', {
      data: { name: 'Alice', roles: ['admin', 'qa'] }, // → application/json
    });
    
    await request.post('/api/v1/avatars', {
      multipart: { file: { name: 'a.png', mimeType: 'image/png', buffer } },
    });

    Коли тіло не текст: gRPC і protobuf

    Не кожен HTTP-обмін узагалі призначений для читання очима. gRPC за власною специфікацією переноситься поверх кадрування HTTP/2, кожен виклик на дроті — це POST, Content-Type починається з application/grpc (інакше сервер SHOULD відповісти 415), а результат їде не в статус-коді: і успіх, і помилка приходять зі :status 200, тоді як справжній код лежить у трейлері grpc-status. Дані серіалізує Protocol Buffers — мовно- і платформо-нейтральний механізм серіалізації структурованих даних, який у gRPC стоїть дефолтом, а не є його властивістю. Через це тіло gRPC-виклику в мережевій панелі не читається як JSON, а побайтове порівняння двох повідомлень некоректне за побудовою: ті самі дані мають багато різних бінарних представлень. Розгорнутий розбір gRPC і protobuf — у главі «SOAP, gRPC і асинхронні API».

    JSON: шість типів і те, чого в ньому немає

    JSON (JavaScript Object Notation) — легкий текстовий формат обміну, незалежний від мови програмування. Типів значень рівно шість: чотири прості — рядок, число, булеве значення, null — і два структурні: обʼєкт і масив. Медіа-тип формату — application/json, і саме він стоїть у Content-Type. JSON, яким обмінюються системи поза замкнутою екосистемою, МАЄ бути закодований у UTF-8.

    {
      "id": 42,
      "name": "Alice",
      "roles": ["admin", "qa"],
      "verified": true,
      "phone": null,
      "profile": { "city": "Kyiv" }
    }

    Половина пасток формату — не в тому, що в ньому є, а в тому, чого немає.

    • Обʼєкт невпорядкований, масив упорядкований. Це сказано в специфікації прямо, тож порядок ключів не гарантований, а порядок елементів масиву — значущий. Асерт, що спирається на послідовність ключів, ненадійний за побудовою.
    • Унікальність імен в обʼєкті — рекомендація, а не вимога: «The names within an object SHOULD be unique». Документ із дублями лишається валідним, а поведінка приймача, за тим самим абзацом, «unpredictable», і багато реалізацій беруть останню пару. Отже, дублікати ключів ловить тест, а не парсер.
    • Тип числа один. Окремих int і float немає, провідні нулі заборонені. Не покладайтеся на те, що поле прийде саме цілим, — це залежить від серіалізації на бекенді.
    • Типу «дата» немає. Дати передають рядком (найчастіше за форматом RFC 3339 / ISO 8601) або числом, і конкретний формат — частина контракту, яку перевіряють явно.
    • Коментарів немає. Коментар у файлі, що претендує на JSON, робить його невалідним.
    • null — це значення, а не відсутність поля. {"phone": null} і {} — різні контракти, і плутати їх у тестах дорого.
    • Рядок береться лише в подвійні лапки, а самі лапки, зворотну скісну риску й керівні символи треба екранувати; літерали true, false, null пишуться лише малими.

    Значенням у JSON може бути обʼєкт або масив, тому реальні відповіді вкладені на кілька рівнів: у прикладі вище roles — масив рядків, profile — вкладений обʼєкт, а шлях до міста виглядає як profile.city. Уміння впевнено читати такі шляхи — щоденний навик при написанні асертів.

    Як читати JSON у тесті й не наступити на серіалізацію

    Вхідний напрямок простий: Content-Type оголошує, що лежить у тілі, а парсинг JSON — окремий крок, який на невалідному вході падає. Порожнє тіло, HTML-сторінка помилки від замість JSON, обірваний потік — усе це для JSON.parse однаково невалідний вхід, і виняток на ньому маскує справжню причину: ви шукатимете баг у форматі даних замість 502, який стоїть у відповіді.

    Вихідний напрямок — те, що ваш код кладе в тіло, — тонший. Date у JSON не «магічний»: у нього є toJSON(), який повертає те саме, що toISOString(), і JSON.stringify() серіалізує саме результат toJSON() — тобто на сервер їде рядок, і зворотний JSON.parse дати не відновить. Ще тонше з undefined: такі значення не є валідними для JSON, тож в обʼєкті поле просто зникає, а в масиві перетворюється на null, і довжина масиву не змінюється. І окремо: JSON.stringify() може повернути не рядок, а undefined, — тоді тіло запиту виявиться порожнім, а сервер відповість 400, і винен буде не він.

    Наша практика (не канон). Порядок дій у тесті специфікації не описують — це наша конструкція, зібрана з двох фактів вище: спершу статус і Content-Type, і лише потім парсинг тіла.

    const res = await request.get('/api/v1/users/42');
    
    expect(res.status()).toBe(200);
    expect(res.headers()['content-type']).toContain('application/json');
    
    const user = await res.json(); // парсимо лише після двох перевірок вище
    expect(user.roles).toContain('admin');

    Типові помилки

    • Виглядає як «REST — це JSON поверх HTTP», а насправді REST — набір із шести обмежень, у яких формат даних не згадано жодного разу. API може віддавати JSON і не бути RESTful, і навпаки.
    • Виглядає як «прийшло 200, отже все добре», а насправді статус описує обробку на рівні протоколу, а не правильність даних: 200 із тілом-помилкою — реальний антипатерн, від якого тест хибно зеленіє.
    • Виглядає як «DELETE двічі повернув 404 — метод не ідемпотентний», а насправді ідемпотентність — про стан сервера, а не про код відповіді. Ресурсу немає і після першого, і після другого виклику; різні коди це не порушують.
    • Виглядає як «PATCH же оновлення, отже ідемпотентний», а насправді специфікація каже прямо: PATCH ані безпечний, ані ідемпотентний. Ідемпотентним можна зробити конкретний запит — умовним, з If-Match.
    • Виглядає як «сервер зламався, бо 400», а насправді тіло не відповідає оголошеному Content-Type, і канонічна відповідь на це — 415. Спершу звірте заголовок із реальним тілом, потім заводьте баг.
    • Виглядає як «поле прийшло порожнім», а насправді null і відсутнє поле — різні контракти. Асерт «поле є» і асерт «поле не null» перевіряють різні речі.
    • Виглядає як «пробіл — це %20», а насправді у application/x-www-form-urlencoded пробіл кодується +, а плюс — %2B. Той самий рядок, зібраний двома шляхами, не збігається побайтово, хоч логічно однаковий.
    • Виглядає як «відповідь змінилася — регресія», а насправді порядок ключів в обʼєкті ніколи не був гарантований, а нове поле у відповіді — сумісна зміна. Ламається тут не API, а асерт на рівність усього тіла.

    Підсумок

    • Ресурс — це «що», ендпоінт — «де», метод — «що зробити», представлення — «в якому вигляді». Чотири різні речі, які легко злипаються в одне слово «API».
    • REST задає обмеження, а не формат. Найважливіше з них для тестів — : кожен запит несе все потрібне сам, тому його можна повторити ізольовано, а стан для сценарію підготувати одним викликом.
    • Контракт відповіді складається щонайменше з трьох частин — статус, Content-Type і структура тіла. Перевірка однієї частини з трьох дає хибно-зелений результат.
    • Безпечність, ідемпотентність і кешованість — три різні осі. Ідемпотентність описує стан сервера після повтору, а не однаковість відповідей, і саме вона відповідає на питання «чи безпечно тут ретраїти».
    • JSON бідний навмисно. Немає дат, коментарів і поділу чисел; порядок ключів не гарантований, null не дорівнює відсутньому полю. Усе, чого формат не дає, домовляються поверх нього — і саме ці домовленості перевіряє тест.

    Далі ця грамотність стає інструментом: розділ про API-тестування бере ту саму модель і вчить будувати на ній перевірки — чек-лист відповіді, схеми й контракти, Postman і Newman, OpenAPI як джерело істини, тест-дизайн для CRUD і негативні сценарії. Тут була платформа, там буде метод.

    Можливі питання

    • «Що таке REST і чи є він стандартом?» Інтервʼюер перевіряє, чи не зводиться відповідь до «це JSON по HTTP». Сильна відповідь називає стиль, автора й хоча б чотири обмеження з шести — і окремо зазначає, що формат даних до стилю не належить.
    • «Чим ендпоінт відрізняється від ресурсу?» Питання на точність мислення. Очікують розведення «концептуальне відображення» проти «конкретна адреса» і висновок: адреса стабільна, вміст за нею змінюється.
    • «Які методи безпечні, а які ідемпотентні? POST ідемпотентний?» Дивляться, чи знаєте ви обидва переліки й чи не плутаєте властивості. Плюс за згадку, що всі безпечні методи ідемпотентні, але не навпаки.
    • «DELETE повторили — прийшов 404. Це порушення ідемпотентності?» Класична пастка. Правильна відповідь розводить стан сервера й код відповіді.
    • «У чому різниця між 401 і 403, між 400 і 422 Питання про негативні сценарії. Очікують не переклад назв, а два формулювання: хто ти проти що тобі можна; не зрозумів проти зрозумів, але так не можна.
    • «Що буде, якщо надіслати JSON-тіло з Content-Type: application/x-www-form-urlencoded Перевіряють, чи розумієте ви роль заголовка. Сильна відповідь називає 415 як канонічний код і згадує, що на практиці трапляється й загальніший 400.
    • «Які типи значень є в JSON і чого в ньому немає?» Базове питання з подвійним дном: перелічити шість типів мало, цінується друга половина — немає дат, коментарів, поділу чисел, а null є значенням.
    • «Ви бачите ?q=search+results. Це пробіл чи плюс?» Дивляться на розуміння кодування: відповідь залежить від того, чи це application/x-www-form-urlencoded; у шляху той самий символ означає літеральний плюс.

    Джерела

    Клієнт-сервер: рамка, у якій усе це живе

    REST — це набір обмежень, а не формат даних

    Безстановість: кожен запит самодостатній

    Ресурс і ендпоінт: чому це не синоніми

    Адреса: що з URL доходить до сервера

    • RFC 3986 — URI Generic Syntax — граматика URI, які компоненти клієнт надсилає серверу, відокремлення фрагмента, нормалізація регістру схеми й хоста, відсутність вимог до структури рядка запиту.
    • WHATWG URL Standard — типові порти особливих схем, які опускають у серіалізації.
    • RFC 9110 — HTTP Semantics — рекомендована нижня межа підтримки довжини URI у 8000 октетів.
    • Apache HTTP Server 2.4 — Core Features (mod_core) — дефолти LimitRequestLine і LimitRequestBody як практичні межі адреси й тіла.
    • nginx — ngx_http_core_module414 на стартовий рядок, що не влазить у буфер.

    Відсоткове кодування в шляху й рядку запиту

    • RFC 3986 — URI Generic Syntax — механізм відсоткового кодування, незарезервовані символи, UTF-8 як проміжний крок, регістронезалежність шістнадцяткових цифр.
    • WHATWG URL Standard — набори символів залежно від компонента й пробіл як + у application/x-www-form-urlencoded.
    • MDN — encodeURIComponent() — поведінка функції для пробілу й &.
    • MDN — encodeURI() — перелік роздільників, які функція не кодує, і правило вибору між двома функціями.

    Метод — це операція, а не частина адреси

    • RFC 9110 — HTTP Semantics — семантика GET, POST, PUT, DELETE, HEAD, OPTIONS; означення безпечного й з переліками; мета поділу; ідемпотентність як властивість стану сервера.
    • MDN — HTTP request methodsPATCH як часткова зміна; зведена таблиця безпечності, ідемпотентності й кешованості: звідси CONNECT серед неідемпотентних і кешованість за замовчуванням лише для GET і HEAD.
    • RFC 5789 — PATCH Method for HTTPPATCH ані безпечний, ані ідемпотентний; умовний запит як спосіб зробити конкретний патч ідемпотентним; різниця тіл PUT і PATCH.
    • MDN — Idempotent (Glossary) — ідемпотентність як однаковий намірений ефект на сервері й приклад із повторним DELETE, у якого коди відповідей різні.
    • MDN — Cross-Origin Resource Sharing (CORS)OPTIONS як preflight-запит браузера.

    Статус-код — частина контракту, а не «щось повернулося»

    • RFC 9110 — HTTP Semantics — статус-код як тризначне число 100–599, класи за першою цифрою, необовʼязковість пояснювальної фрази, вимога WWW-Authenticate при 401, 415 як відмова через формат тіла, межа між 4xx і 5xx.
    • MDN — HTTP response status codes — значення 200, 201 з Location, 204 без тіла, 400 проти 422, 401 проти 403, 404, 405, 409, 429 з Retry-After.
    • RFC 9112 — HTTP/1.1 (message syntax) — вимога до клієнта ігнорувати пояснювальну фразу статусу.
    • RFC 6585 — Additional HTTP Status Codes — документ, який вводить 429.

    Тіло повідомлення й типи вмісту

    • MDN — HTTP messages — тіло як частина повідомлення після заголовків; які методи запиту мають тіло; тіло присутнє в більшості відповідей.
    • RFC 9110 — HTTP SemanticsContent-Type і Content-Length, тіло в GET без визначеної семантики, 415 як відповідь на непідтримуваний медіа-тип.
    • RFC 7578 — Returning Values from Forms: multipart/form-data — будова multipart/form-data: частини з міні-заголовками, boundary і filename.
    • RFC 2046 — MIME Part Two: Media Types — роздільник як два дефіси плюс значення boundary, завершальний роздільник, заборона збігу з вмістом.
    • WHATWG URL Standardapplication/x-www-form-urlencoded як список пар «імʼя-значення» з пробілом у вигляді +.
    • Playwright — class APIRequestContext — передавання файлу обʼєктом замість ручного складання multipart.

    Коли тіло не текст: gRPC і protobuf

    JSON: шість типів і те, чого в ньому немає

    • RFC 8259 — The JavaScript Object Notation (JSON) Data Interchange Format — чотири прості й два структурні типи, невпорядкований обʼєкт проти впорядкованого масиву, SHOULD на унікальність імен і непередбачувана поведінка при дублях, запис чисел і рядків, відсутність типу «дата», null як значення, UTF-8 і медіа-тип application/json.
    • RFC 3339 — Date and Time on the Internet: Timestamps — формат дати-часу, яким передають дати всередині JSON-рядка.

    Як читати JSON у тесті й не наступити на серіалізацію

    • MDN — JSON (JavaScript reference) — парсинг тексту як окремий крок, що падає на невалідному вході.
    • RFC 9110 — HTTP SemanticsContent-Type як оголошення формату тіла, на яке спирається рішення парсити.
    • MDN — JSON.stringify() — серіалізація через toJSON(), доля undefined в обʼєкті й масиві, повернення undefined замість рядка.

    Пояснення

    «Поясни» працює з власним API-ключем Anthropic: запит іде з вашого браузера прямо до Anthropic.

    Свій API-ключ (BYOK)

    Вставте власний ключ Anthropic — пояснення працюватиме на реальній моделі. Ключ зберігається лише у вашому браузері (localStorage), нікуди не надсилається, крім api.anthropic.com, і не логується.