Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

Питання на співбесіді з API

Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.

100 питань

Запит складається з рядка запиту, заголовків і (необов'язково) тіла:

POST /api/orders HTTP/1.1
Host: shop.example.com
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJhbGciOi...
Idempotency-Key: 8f14e45f-ceea-467f-a0d6-2a1e6c0c4f11

{"product_id": 42, "qty": 2}
  • метод (GET, POST, PATCH...) - що зробити;
  • шлях і рядок запиту - з яким ресурсом;
  • заголовки - метадані: формат, автентифікація, кешування;
  • тіло - дані (для GET тіла зазвичай немає).

Відповідь - рядок статусу, заголовки, тіло:

HTTP/1.1 201 Created
Content-Type: application/json
Location: /api/orders/1057
Cache-Control: no-store

{"data": {"id": 1057, "status": "new"}}

Заголовки, які варто знати для API:

Заголовок Навіщо
Content-Type формат тіла, яке надсилається
Accept формат, який клієнт хоче отримати
Authorization облікові дані (Bearer-токен)
Location адреса створеного ресурсу (з 201) чи статусу операції (з 202)
Cache-Control, ETag кешування й умовні запити
Retry-After коли повторити (з 429, 503)
X-Request-Id / traceparent наскрізний ідентифікатор для логів і трасування

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

  • Content-Type не відповідає тілу - сервер не розбере JSON, надісланий як text/plain;
  • відсутній Accept: application/json у запитах до Laravel - при помилці валідації замість JSON 422 прийде редирект на попередню сторінку;
  • статус 200 з {"error": ...} у тілі - клієнти, проксі й моніторинг орієнтуються на код статусу, тож помилка має бути помилкою на рівні HTTP;
  • власні заголовки з префіксом X- - застарілий звичай (RFC 6648); нові заголовки називають без нього.

Налагодження: curl -i показує заголовки відповіді, curl -v - і запиту; у браузері - вкладка Network.

Докладніше в документації: Огляд HTTP

Два заголовки, які часто плутають, описують різні напрямки:

  • Content-Type - формат тіла цього повідомлення. У запиті - що надсилає клієнт, у відповіді - що повертає сервер;
  • Accept - формат, який клієнт готовий отримати у відповідь.
POST /api/reports
Content-Type: application/json
Accept: text/csv

{"from": "2026-09-01", "to": "2026-09-30"}

Клієнт надсилає JSON, а хоче отримати CSV.

Узгодження вмісту (content negotiation) - сервер обирає формат відповіді за Accept:

Accept: application/json;q=1.0, text/csv;q=0.5

q - вага переваги від 0 до 1. Сервер повертає найкращий з підтримуваних форматів, а якщо не підтримує жоден - 406 Not Acceptable. Якщо сервер не розуміє формат тіла запиту - 415 Unsupported Media Type.

Інші заголовки узгодження:

  • Accept-Language - мова (повідомлення помилок, перекладені поля);
  • Accept-Encoding - стиснення (gzip, br, zstd);
  • у відповіді - Vary: Accept, Accept-Language, щоб кеші й CDN зберігали окремі копії для різних варіантів.

Медіатипи для API:

  • application/json - основний;
  • application/problem+json - помилки за RFC 9457;
  • application/vnd.api+json - JSON:API;
  • multipart/form-data - завантаження файлів;
  • власні «вендорні» типи з версією (application/vnd.shop.v2+json) - один зі способів версіонування.

У Laravel:

  • $request->expectsJson() / wantsJson() перевіряють Accept - від нього залежить, чи поверне Laravel помилки валідації й винятки як JSON;
  • $request->isJson() - чи тіло запиту JSON (за Content-Type);
  • $request->getAcceptableContentTypes() - список з урахуванням ваг.

Практичний підсумок: API-клієнти мають завжди надсилати Accept: application/json, а сервер - завжди виставляти правильний Content-Type відповіді з кодуванням (application/json для JSON вважається UTF-8 за специфікацією).

Докладніше в документації: Узгодження вмісту

Пагінація зі зміщенням (offset) - номер сторінки:

GET /api/posts?page=3&per_page=20
SELECT * FROM posts ORDER BY created_at DESC LIMIT 20 OFFSET 40;

Курсорна пагінація - «продовжити після цього запису»:

GET /api/posts?cursor=eyJjcmVhdGVkX2F0IjoiMjAyNi0wOS0zMCJ9
SELECT * FROM posts WHERE created_at < '2026-09-30 12:00:00' ORDER BY created_at DESC LIMIT 20;

Курсор - закодоване значення ключа сортування останнього елемента попередньої сторінки.

Порівняння:

Зміщення Курсор
перехід на довільну сторінку так ні, лише вперед/назад
загальна кількість сторінок так (потрібен COUNT(*)) зазвичай ні
швидкість на далеких сторінках падає: база читає й відкидає OFFSET рядків стабільна: пошук за індексом
нові записи під час гортання дублікати й пропуски коректно

Проблема зміщення з даними, що змінюються: поки користувач на сторінці 2, з'явилося 5 нових постів. Сторінка 3 тепер починається на 5 записів «раніше» - частину постів він побачить двічі. Курсор прив'язаний до значення, а не до позиції, і такої проблеми не має.

У Laravel:

Post::latest()->paginate(20);         // зміщення + COUNT(*), номери сторінок
Post::latest()->simplePaginate(20);   // зміщення без COUNT(*), лише «далі/назад»
Post::latest()->cursorPaginate(20);   // курсор

cursorPaginate вимагає, щоб сортування було за унікальною комбінацією колонок (зазвичай додають id як останній ключ) і щоб для неї був індекс.

Що обрати:

  • адмінки, таблиці з переходом на сторінку N, невеликі обсяги - зміщення;
  • стрічки, нескінченний скрол, мобільні застосунки, синхронізація й експорт великих обсягів - курсор.

У відповіді API варто віддавати посилання на наступну сторінку (links.next чи заголовок Link), а не змушувати клієнта збирати URL самостійно - тоді механізм пагінації можна змінити без зміни клієнтів.

Докладніше в документації: Laravel: курсорна пагінація

HEAD - те саме, що GET, але без тіла відповіді: сервер повертає лише статус і заголовки.

HEAD /api/exports/2026-09.csv

HTTP/1.1 200 OK
Content-Type: text/csv
Content-Length: 48211337
Last-Modified: Wed, 01 Oct 2026 03:00:00 GMT
ETag: "a1b2c3"

Навіщо:

  • дізнатися розмір файлу перед завантаженням (смуга прогресу, перевірка місця);
  • перевірити, чи ресурс існує чи змінився (ETag, Last-Modified) без передачі вмісту;
  • перевірка посилань - сканери битих посилань і моніторинг доступності.

Заголовки відповіді на HEAD мають бути такими самими, як для GET. У Laravel маршрути GET автоматично відповідають і на HEAD - фреймворк просто відкидає тіло.

OPTIONS - запит про можливості ресурсу: які методи підтримуються.

OPTIONS /api/orders/42

HTTP/1.1 204 No Content
Allow: GET, PATCH, DELETE

Головне застосування на практиці - попередній запит CORS (preflight). Перед «непростим» запитом з іншого джерела (методи PUT/PATCH/DELETE, заголовок Authorization, Content-Type: application/json) браузер сам надсилає OPTIONS:

OPTIONS /api/orders/42
Origin: https://app.example.com
Access-Control-Request-Method: PATCH
Access-Control-Request-Headers: authorization, content-type

Сервер відповідає дозволами (Access-Control-Allow-*), і лише потім іде справжній запит. У Laravel це робить middleware HandleCors з config/cors.php.

Що варто знати:

  • обидва методи безпечні й ідемпотентні - не повинні змінювати стан;
  • preflight - додатковий запит на кожен «непростий» запит з іншого джерела. Access-Control-Max-Age дозволяє браузеру кешувати дозвіл і не повторювати OPTIONS щоразу;
  • автентифікація для OPTIONS: браузер не надсилає Authorization у preflight. Якщо middleware автентифікації відхиляє OPTIONS з 401, CORS-запити ламаються - preflight має оброблятися до автентифікації (як це й робить глобальний HandleCors);
  • 405 Method Not Allowed разом із заголовком Allow - правильна відповідь на непідтримуваний метод.

Докладніше в документації: Метод HEAD

JSON добре стискається: повторювані ключі й структура дають зменшення у 5-10 разів. Для мобільних клієнтів і великих списків це суттєва економія часу й трафіку.

Як це працює:

GET /api/products
Accept-Encoding: zstd, br, gzip

HTTP/1.1 200 OK
Content-Type: application/json
Content-Encoding: br
Vary: Accept-Encoding
  1. клієнт перелічує в Accept-Encoding алгоритми, які розуміє;
  2. сервер обирає один, стискає тіло й повідомляє про це в Content-Encoding;
  3. клієнт (браузер, fetch, HTTP-клієнт) розпаковує автоматично.

Vary: Accept-Encoding обов'язковий для кешів і CDN - інакше стиснена копія може дістатися клієнту, що її не розуміє.

Алгоритми:

Алгоритм Особливість
gzip підтримується всюди, швидкий, середнє стиснення
br (Brotli) краще стиснення за gzip, особливо для тексту; підтримка в усіх сучасних браузерах
zstd (Zstandard) дуже швидке стиснення й розпакування при хорошому коефіцієнті; підтримується в Chrome і Firefox, Safari - з 2025 року

Сервер обирає найкращий з того, що підтримує клієнт, і зазвичай лишає gzip як запасний варіант.

Де вмикати: не в PHP, а на рівні вебсервера чи CDN - Nginx (gzip, модуль brotli), Caddy (encode zstd gzip), Cloudflare. Там стиснення ефективніше й не займає воркери PHP.

Що варто знати:

  • малі відповіді не стискають: для тіла в кількасот байтів накладні витрати більші за виграш (типовий поріг - 1 КБ);
  • вже стиснене (зображення, архіви, PDF) повторно не стискають;
  • рівень стиснення - компроміс з процесором: для динамічних відповідей - середній рівень, максимальний - лише для статики, стисненої заздалегідь;
  • BREACH: стиснення відповідей, що містять і секрет (CSRF-токен), і дані, підконтрольні атакувальнику, на HTTPS дає змогу вгадувати секрет за розміром відповіді. Для JSON API з токенами в заголовках, а не в тілі, ризик невеликий, але про атаку варто знати;
  • тіло запиту клієнти зазвичай не стискають - Content-Encoding у запиті сервер має підтримувати окремо.

Перевірка: curl -H 'Accept-Encoding: br' -I https://api.example.com/products - у відповіді має бути Content-Encoding.

Докладніше в документації: Стиснення в HTTP

OpenAPI (раніше Swagger) - стандартний машиночитний формат опису HTTP API у YAML чи JSON: ендпойнти, параметри, тіла запитів і відповідей, коди статусу, автентифікація.

openapi: 3.1.0
info:
  title: Vacancies API
  version: 1.0.0
paths:
  /vacancies/{id}:
    get:
      summary: Отримати вакансію
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: integer }
      responses:
        '200':
          description: Вакансія
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Vacancy' }
        '404':
          description: Не знайдено
components:
  schemas:
    Vacancy:
      type: object
      required: [id, title]
      properties:
        id: { type: integer }
        title: { type: string }
        salary_from: { type: [integer, 'null'] }

Що дає специфікація, крім документації:

  • інтерактивна документація - Swagger UI, Redoc, Scalar, Stoplight Elements з кнопкою «спробувати запит»;
  • генерація клієнтів - типізовані SDK для TypeScript, PHP, мобільних застосунків;
  • моки - фронтенд працює з фейковим сервером до готовності бекенду;
  • тести й валідація - перевірка, що реальні відповіді відповідають опису;
  • лінтинг - автоматична перевірка стилю й помилок у дизайні;
  • виявлення змін, що ламають клієнтів, - порівняння версій специфікації в CI;
  • імпорт у Postman, Insomnia, Bruno, API-шлюзи.

Версії:

  • 3.0 - найпоширеніша в старих інструментах;
  • 3.1 (2021) - повна сумісність із JSON Schema 2020-12;
  • 3.2 (вересень 2025) - зворотно сумісне розширення 3.1: потокові медіатипи (SSE, JSON Lines), нові поля для тегів, метод QUERY тощо.

Для нових проєктів - 3.1 чи 3.2, з перевіркою, що потрібні інструменти їх підтримують.

Головна цінність - єдине джерело правди про контракт між бекендом і клієнтами, замість вікі-сторінок, які розходяться з кодом через тиждень.

У Laravel специфікацію найчастіше генерують з коду (Scramble, Scribe), а не пишуть вручну.

Докладніше в документації: Специфікація OpenAPI 3.2.0

Code-first - спершу пишеться код, специфікація генерується з нього (атрибутами, аналізом коду):

контролери й ресурси Laravel  →  Scramble/Scribe  →  openapi.json  →  документація

Design-first - спершу пишеться специфікація (контракт), її обговорюють і погоджують, а потім за нею пишуть код і клієнтів:

openapi.yaml (рев'ю)  →  моки для фронтенду  →  реалізація бекенду  →  перевірка відповідності

Переваги code-first:

  • специфікація не розходиться з кодом - вона з нього й береться;
  • швидкий старт, мало додаткової роботи;
  • зручно для невеликих команд, де бекенд і фронтенд пишуть ті самі люди.

Недоліки code-first:

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

Переваги design-first:

  • контракт - до коду: фронтенд, мобільна команда й партнери погоджують API заздалегідь і працюють паралельно з моками;
  • кращий дизайн: API проєктують з боку споживача, з рев'ю, як код;
  • генерація серверних заготовок і валідація запитів за специфікацією.

Недоліки design-first:

  • специфікацію треба підтримувати вручну - і перевіряти, що реалізація їй відповідає (тести контракту, валідація відповідей у тестах), інакше вона застаріває;
  • більше процесу й інструментів.

Що обирають на практиці:

  • внутрішнє API одного продукту (Laravel + SPA однієї команди) - code-first зі Scramble: мінімум зусиль, завжди актуально;
  • публічне API, партнерські інтеграції, кілька команд-споживачів - design-first або гібрид: дизайн погоджується в специфікації, а згенерована з коду специфікація порівнюється з погодженою в CI;
  • будь-який підхід виграє від лінтингу специфікації і перевірки змін, що ламають клієнтів, у CI.

Докладніше в документації: Документація OpenAPI для початківців

Scramble генерує специфікацію OpenAPI з коду Laravel автоматично, без анотацій над кожним методом. Він аналізує:

  • маршрути - шляхи, методи, параметри маршруту й прив'язку моделей;
  • валідацію - правила з Form Request чи $request->validate() стають описом тіла запиту й параметрів (required|email|max:255 → обов'язковий рядок формату email);
  • відповіді - API-ресурси (JsonResource), повернені моделі, пагінацію, response()->json(...);
  • помилки - 422 для валідації, 404 для прив'язки моделей, 403 для авторизації, 401 для автентифікації.
composer require dedoc/scramble

Після встановлення документація доступна за адресою /docs/api (інтерфейс на основі Stoplight Elements), а сама специфікація - /docs/api.json.

Доповнення там, де аналізу коду недостатньо:

/**
 * Список вакансій.
 *
 * Повертає опубліковані вакансії, найновіші першими.
 */
public function index(IndexVacancyRequest $request)
{
    // ...
}

PHPDoc-коментар стає описом операції. Для складних випадків - атрибути Scramble й розширення.

Що варто налаштувати:

  • доступ: за замовчуванням документація відкрита лише в локальному оточенні, у продакшені доступ визначає gate viewApiDocs;
  • які маршрути документувати (за замовчуванням - з префіксом api);
  • автентифікація: опис схеми (Bearer-токен Sanctum), щоб кнопка «спробувати» працювала;
  • експорт: php artisan scramble:export записує специфікацію у файл - для генерації клієнтів і перевірок у CI.

Альтернатива - Scribe: генерує документацію й колекції Postman, бере опис з анотацій і може робити справжні запити до ендпойнтів, щоб отримати приклади відповідей.

Обмеження автоматичної генерації:

  • точність залежить від коду: динамічні масиви в toArray(), умовна логіка, дані з сервісів можуть описатися неточно - результат треба переглядати;
  • якість опису - на вас: назви полів, приклади, пояснення бізнес-правил генератор не придумає;
  • вбудовані JSON:API-ресурси Laravel 13 - новий формат; варто перевірити, що версія генератора їх підтримує.

Докладніше в документації: Scramble

Laravel має набір перевірок для JSON-відповідей. Головне - обирати перевірку під те, що саме гарантує API.

it('returns a vacancy', function () {
    $vacancy = Vacancy::factory()->create(['title' => 'Laravel Developer', 'salary_from' => 3000]);

    $this->getJson("/api/vacancies/{$vacancy->id}")
        ->assertOk()
        ->assertJsonPath('data.title', 'Laravel Developer')
        ->assertJsonPath('data.salary_from', 3000)
        ->assertJsonStructure([
            'data' => ['id', 'title', 'salary_from', 'company' => ['id', 'name']],
        ]);
});

Основні методи:

Метод Що перевіряє
assertJson([...]) відповідь містить ці дані (інші поля дозволені)
assertExactJson([...]) відповідь дорівнює цьому JSON
assertJsonPath('data.title', '...') значення за шляхом
assertJsonStructure([...]) наявність ключів (без значень)
assertExactJsonStructure([...]) ключі - і жодних зайвих
assertJsonCount(20, 'data') кількість елементів
assertJsonMissingPath('data.password') поля немає
assertJsonValidationErrors(['email']) помилки валідації для полів

Методи запиту: getJson, postJson, patchJson, deleteJson - автоматично додають Accept: application/json, тож помилки валідації приходять як JSON 422, а не редирект.

Що варто перевіряти в API-тестах:

  • контракт: структура відповіді й типи - те, на що покладаються клієнти;
  • приховані поля: assertJsonMissingPath('data.password_hash') - захист від випадкового витоку при зміні ресурсу;
  • коди статусу для кожного сценарію: 201 при створенні, 422 при помилках, 403 для чужого ресурсу, 404 для неіснуючого;
  • авторизацію: запит від іншого користувача (actingAs($stranger)) - найчастіше забута перевірка;
  • пагінацію й фільтри: assertJsonCount, перевірка, що фільтр справді відсікає записи.

Пастки:

  • assertJson не перевіряє відсутність полів - нове випадкове поле в ресурсі тест не помітить. Для захисту контракту - assertExactJsonStructure або перевірка відповідності специфікації OpenAPI;
  • типи: assertJsonPath('data.price', 100) і '100.00' - різні значення; рішення про формат грошей має бути зафіксоване тестом;
  • дати порівнюють у форматі, який віддає API (ISO 8601 з поясом), а не як об'єкти Carbon.

Для складних перевірок - fluent-API assertJson(fn (AssertableJson $json) => ...).

Докладніше в документації: Laravel: тестування JSON API

Згенерований перелік ендпойнтів з параметрами - лише довідник. Розробник, що інтегрується з API, має відповіді на питання «з чого почати» і «що робити, коли щось пішло не так».

1. Швидкий старт: як отримати ключ, перший запит, який можна скопіювати й виконати за хвилину:

curl https://api.example.com/v1/vacancies?city=kyiv \
  -H "Authorization: Bearer $TOKEN" \
  -H "Accept: application/json"

2. Автентифікація: як отримати й оновити токен, термін дії, області доступу (scopes).

3. Приклади - для кожного запиту й відповіді. Реалістичні дані, а не "string" і 0. В OpenAPI - поля example/examples у схемах і медіатипах. Кілька прикладів для різних сценаріїв (вакансія із зарплатою і без).

4. Помилки - не менш детально, ніж успіх:

  • формат помилки (Problem Details чи власний) з прикладами;
  • коди помилок бізнес-логіки й що з ними робити (insufficient_balance - поповнити рахунок);
  • які помилки тимчасові й можна повторити (429, 503), а які ні.

5. Загальні правила API - один раз для всіх ендпойнтів:

  • пагінація, фільтрація, сортування;
  • формати дат, грошей, ідентифікаторів;
  • ліміти частоти й заголовки, що їх показують;
  • ідемпотентність і повтори;
  • версіонування й політика змін.

6. Сценарії (гайди): типові задачі, що складаються з кількох запитів - «створити замовлення й оплатити», «синхронізувати каталог». Довідник ендпойнтів їх не пояснює.

7. Журнал змін (changelog) і дати застарівання.

8. SDK і колекції: офіційні клієнти, колекція Postman/Bruno, посилання на специфікацію OpenAPI для генерації власних клієнтів.

Якість, яку легко перевірити:

  • новий розробник робить перший успішний запит без запитань до команди;
  • кожен ендпойнт має приклад відповіді й перелік можливих помилок;
  • приклади в документації перевіряються автоматично (контрактні тести, валідація за специфікацією) - інакше вони застаріють першими.

Інтерактивність: кнопка «спробувати» в документації (Swagger UI, Scalar, Stoplight Elements) з тестовим оточенням (sandbox) пришвидшує інтеграцію - але ніколи з продакшен-даними.

Докладніше в документації: Документація OpenAPI для початківців

Три поширені стилі API розв'язують різні задачі.

REST - ресурси за URL і стандартні методи HTTP:

GET /api/orders/42
GET /api/orders/42/items
  • сильні сторони: простота, HTTP-кешування (CDN, ETag), зрозумілі коди відповіді, будь-який клієнт (навіть curl);
  • слабкі: клієнт отримує фіксовану форму відповіді - або зайві поля (overfetching), або кілька запитів, щоб зібрати екран (underfetching).

GraphQL - один ендпойнт, клієнт сам описує, які поля потрібні:

query {
  order(id: 42) { number total items { name qty } customer { name } }
}
  • сильні сторони: один запит на екран, сувора схема з типами, зручно для кількох клієнтів з різними потребами (веб, мобільний застосунок);
  • слабкі: складніше кешування (зазвичай POST на один URL), ризик дорогих запитів, N+1 на сервері, складніші авторизація й обмеження частоти.

gRPC - виклик віддалених процедур поверх HTTP/2 з бінарним форматом Protocol Buffers і згенерованими клієнтами:

  • сильні сторони: швидкість і компактність, суворий контракт у .proto, двобічний стримінг;
  • слабкі: браузер не викликає gRPC напряму (потрібен gRPC-Web чи шлюз), бінарні повідомлення важче налагоджувати.

Як обирати:

Ситуація Стиль
публічне API, інтеграції партнерів, вебхуки REST
багато клієнтів з різними екранами, складні зв'язані дані GraphQL
внутрішні сервіси між собою, високе навантаження, стримінг gRPC
прості дії на кшталт «надіслати лист» RPC поверх HTTP (POST /api/send-invoice)

Практичне зауваження: для типового Laravel-застосунку з одним фронтендом REST (чи Inertia без окремого API) майже завжди простіший. GraphQL і gRPC окупаються, коли їхні сильні сторони справді потрібні, - а не «бо модно».

Докладніше в документації: Вступ до GraphQL

Схема - контракт API: які типи є, які поля в них і що можна запитати. Пишеться мовою SDL:

type User {
  id: ID!
  name: String!
  email: String
  posts: [Post!]!
}

type Post {
  id: ID!
  title: String!
  author: User!
}

type Query {
  user(id: ID!): User
  posts(first: Int = 10): [Post!]!
}

type Mutation {
  createPost(title: String!, body: String!): Post!
}

! - поле не може бути null. [Post!]! - список, який сам не null і не містить null.

Запит (query) - читання. Клієнт вибирає лише потрібні поля, включно з вкладеними:

query {
  user(id: 7) {
    name
    posts { title }
  }
}

Відповідь повторює форму запиту: { "data": { "user": { "name": "Оля", "posts": [...] } } }.

Мутація (mutation) - зміна даних. Синтаксис як у запиту, але виконується послідовно, а результат - змінений об'єкт:

mutation {
  createPost(title: "Привіт", body: "...") { id title }
}

Підписка (subscription) - потік подій у реальному часі, зазвичай через WebSocket.

Резолвер - функція на сервері, що повертає значення поля. Для кожного поля в запиті сервер викликає його резолвер. Простим полям (name) достатньо значення з батьківського об'єкта, а для зв'язків (posts) резолвер іде в базу.

Відмінності від REST, які варто пам'ятати:

  • одна адреса (/graphql) і зазвичай метод POST;
  • помилки не через коди HTTP: відповідь часто має статус 200, а помилки лежать у масиві errors поряд із частковими даними в data;
  • інтроспекція: схему можна запитати через сам API - на цьому побудовані автодоповнення в GraphiQL і генератори типів для клієнтів.

У Laravel найпоширеніший пакет - Lighthouse: схема описується в .graphql-файлі, а директиви (@all, @find, @paginate, @create) прив'язують поля до моделей Eloquent без ручних резолверів.

Докладніше в документації: Схеми й типи GraphQL

Вебхук - HTTP-запит, який ваш сервіс надсилає на URL клієнта, коли щось сталося: замовлення оплачене, вакансію опубліковано. Клієнт не опитує API, а отримує подію сам.

Що надсилати:

{
  "id": "evt_01J9Z3K8",
  "type": "order.paid",
  "created_at": "2026-10-04T10:15:00Z",
  "api_version": "2026-09-01",
  "data": {
    "object": { "id": 42, "status": "paid", "total": "1250.00", "currency": "UAH" }
  }
}
  • id події - унікальний: одержувач за ним відкидає дублікати;
  • type - назва події у форматі ресурс.дія; клієнт підписується лише на потрібні;
  • час події - щоб одержувач міг розібратися з порядком;
  • версія формату - щоб змінювати структуру, не ламаючи наявних інтеграцій;
  • дані: або повний знімок об'єкта («товстий» вебхук), або лише ідентифікатор («тонкий», одержувач сам запитує актуальний стан через API).

Обов'язкові складники надійного вебхука:

  • підпис запиту (HMAC з секретом одержувача) і мітка часу - щоб одержувач перевірив, що запит від вас і не повторений;
  • HTTPS для URL одержувача;
  • доставка «щонайменше один раз»: при помилці чи тайм-ауті - повторні спроби з наростаючою затримкою. Одержувач має бути готовим до дублікатів;
  • швидка відповідь: одержувач підтверджує прийом кодом 2xx одразу, а обробляє асинхронно. Документуйте тайм-аут (кілька секунд);
  • журнал доставок у кабінеті: які події пішли, з яким кодом відповіді, кнопка «надіслати ще раз».

Відправлення в Laravel - лише через чергу. HTTP-запит до чужого сервера в обробнику запиту користувача - затримки й падіння, якщо одержувач повільний чи недоступний. Готове рішення - пакет spatie/laravel-webhook-server: черга, підпис, повтори з затримкою, події про невдалі доставки.

Документація для одержувачів: перелік типів подій із прикладами, як перевіряти підпис, політика повторів, тестові події з кабінету.

Докладніше в документації: Вебхуки Stripe

REST працює за схемою «запит - відповідь»: клієнт питає, сервер відповідає. Коли клієнту потрібно дізнаватися про зміни одразу, без постійного опитування, використовують постійне з'єднання.

Варіанти:

  • опитування (polling) - запит раз на N секунд. Найпростіше, але створює багато порожніх запитів і затримку до N секунд;
  • Server-Sent Events (SSE) - однобічний потік від сервера до клієнта через звичайне HTTP-з'єднання. Автоматичне перепідключення вбудоване в браузер. Добре для сповіщень, прогресу, стрічок, відповідей LLM;
  • WebSocket - двобічний канал. Потрібен, коли й клієнт часто надсилає дані: чат, спільне редагування, присутність користувачів онлайн.

Laravel Reverb - власний WebSocket-сервер Laravel, сумісний із протоколом Pusher. Застосунок транслює події, а клієнти підписуються через Laravel Echo:

php artisan install:broadcasting --reverb
php artisan reverb:start
class OrderShipped implements ShouldBroadcast
{
    public function __construct(public Order $order) {}

    public function broadcastOn(): array
    {
        return [new PrivateChannel("orders.{$this->order->user_id}")];
    }
}
Echo.private(`orders.${userId}`).listen('OrderShipped', (event) => {
  updateStatus(event.order);
});

Приватні канали авторизуються в routes/channels.php - Reverb пускає лише тих, кому дозволено слухати канал.

Що враховувати в продакшені:

  • Reverb - окремий довгоживучий процес (Supervisor, контейнер), за зворотним проксі з підтримкою WebSocket;
  • ліміт відкритих файлів ОС - кожне з'єднання займає дескриптор;
  • горизонтальне масштабування - кілька серверів Reverb обмінюються повідомленнями через Redis (REVERB_SCALING_ENABLED);
  • події через чергу: трансляція з ShouldBroadcast іде через чергу, тож без воркера повідомлення не надходять.

SSE в Laravel - response()->eventStream() у звичайному маршруті: без окремого сервера, але кожен відкритий потік тримає процес PHP.

Докладніше в документації: Laravel Reverb

API-шлюз - окремий сервіс-«вхідні двері» перед вашими API. Клієнти звертаються лише до нього, а він маршрутизує запити до внутрішніх сервісів.

клієнти → API-шлюз → сервіс замовлень
                   → сервіс користувачів
                   → сервіс платежів

Що зазвичай робить шлюз:

  • маршрутизація: /api/orders/* - до одного сервісу, /api/users/* - до іншого;
  • автентифікація: перевірка токенів чи API-ключів один раз на вході, далі - довірений заголовок з ідентифікатором користувача;
  • обмеження частоти і квоти для клієнтів і тарифів;
  • TLS-термінація, CORS, стиснення;
  • кешування відповідей;
  • трансформація: перетворення форматів, об'єднання відповідей кількох сервісів в одну;
  • спостережуваність: централізовані журнали, метрики, трасування запитів;
  • версіонування і поступове перемикання трафіку між версіями сервісу.

Приклади: Kong, Tyk, AWS API Gateway, Azure API Management, Cloudflare (частково), Traefik і Nginx як простіші варіанти.

Коли шлюз доречний:

  • кілька сервісів, які мають виглядати як одне API;
  • публічне API з ключами, тарифами, квотами й порталом для розробників;
  • потрібно централізовано застосовувати політики, а не дублювати їх у кожному сервісі.

Коли він зайвий: один моноліт на Laravel. Маршрутизація, автентифікація (Sanctum, Passport), обмеження частоти (throttle) уже є у фреймворку, а зовнішній шлюз лише додасть ще одну ланку, яка може впасти.

Ризики шлюзу:

  • єдина точка відмови і вузьке місце - потребує резервування;
  • «розумний шлюз»: бізнес-логіка, що поступово переїжджає в конфігурацію шлюзу, стає некерованою. Шлюз має займатися наскрізними задачами, а не правилами предметної області;
  • додаткова затримка на кожен запит.

Споріднений патерн - BFF (backend for frontend): окремий шлюз під кожен тип клієнта, що збирає дані саме під його екрани.

Докладніше в документації: Патерн API Gateway

Питання з реальних технічних співбесід - 100 питань у 7 темах, розібраних із відповідями. Нижче - розбивка за рівнями та темами, якщо хочете звузити підготовку.

Рівні
Junior 35 Middle 35 Senior 30

Готуєтесь до співбесіди не просто так: зараз на сайті 146 відкритих вакансій Laravel і PHP. Переглянути вакансії