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

Middle: питання на співбесіді з теми «Документація й контракти»

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

5 питань

Обидва генерують документацію API з коду Laravel, але різними способами.

Scramble:

  • статичний аналіз коду: читає маршрути, Form Request, правила валідації, API-ресурси й типи повернення - без анотацій;
  • результат - специфікація OpenAPI 3.1 і веб-інтерфейс документації на її основі;
  • документація оновлюється автоматично разом з кодом;
  • доповнення через PHPDoc і атрибути; платна версія підтримує популярні пакети (Laravel Data, Query Builder).

Scribe:

  • комбінація джерел: правила валідації з Form Request, PHPDoc-анотації (@group, @bodyParam, @response) і, за бажанням, реальні запити до ендпойнтів у локальному оточенні, щоб отримати справжні приклади відповідей;
  • генерує HTML-документацію з прикладами коду кількома мовами й кнопкою «Try It Out», а також колекцію Postman і специфікацію OpenAPI;
  • документація генерується командою (php artisan scribe:generate) - статичні файли, які можна викласти будь-де.

Як обрати:

Критерій Scramble Scribe
зусилля на старті мінімальні більше анотацій
актуальність завжди з коду після перегенерації
реальні приклади відповідей з типів і ресурсів можуть братися з живих запитів
основний артефакт OpenAPI-специфікація HTML-документація + Postman
точність при складній логіці залежить від аналізу коду контролюється анотаціями

Практичні поради:

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

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

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

Fluent-перевірки дають змогу описати відповідь повністю: значення, типи, вкладені структури й відсутність зайвих полів.

use Illuminate\Testing\Fluent\AssertableJson;

$this->getJson('/api/vacancies?filter[city]=kyiv')
    ->assertOk()
    ->assertJson(fn (AssertableJson $json) => $json
        ->has('data', 3, fn (AssertableJson $vacancy) => $vacancy
            ->where('city', 'kyiv')
            ->whereType('id', 'integer')
            ->whereType('salary_from', 'integer|null')
            ->has('company', fn (AssertableJson $company) => $company
                ->hasAll(['id', 'name'])
                ->missing('owner_email')
                ->etc()
            )
            ->etc()
        )
        ->has('meta')
        ->has('links')
    );

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

  • where('key', $value) - точне значення; можна передати замикання для власної умови;
  • whereType('key', 'string') - тип (string, integer, array, null, через | - кілька);
  • has('key'), has('items', 3) - наявність і кількість;
  • has('data', 3, fn ...) - кількість і перевірка першого елемента колекції;
  • each(fn ...) - перевірка кожного елемента;
  • hasAll([...]), hasAny([...]), missing('key'), missingAll([...]).

Найважливіше - строгість за замовчуванням. Кожен рівень, перевірений через замикання, вимагає, щоб усі ключі на цьому рівні були перевірені. Незгадане поле - провал тесту. etc() явно дозволяє «інші поля теж можуть бути».

Це захищає від головного ризику API: нове поле, що випадково потрапило у відповідь (наприклад, хтось додав модель цілком замість ресурсу, і в JSON з'явилися внутрішні поля). Без etc() тест це помітить.

Коли що обирати:

  • простий тест одного значення - assertJsonPath;
  • контракт ресурсу (усі поля й типи, нічого зайвого) - fluent-перевірки без etc() на ключових рівнях;
  • дуже великі відповіді - перевірка відповідності специфікації OpenAPI (бібліотеки валідації відповідей за схемою), щоб не дублювати опис контракту в тестах і в документації.

Пастка: has('data', 3, ...) перевіряє замиканням лише перший елемент. Для перевірки всіх - has('data', 3) і окремо ->each(...) або ->has('data.0', ...)/'data.1' для конкретних позицій.

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

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

Варіант 1 - лише типи (openapi-typescript + openapi-fetch):

npx openapi-typescript ./openapi.json -o ./src/api/schema.d.ts
import createClient from 'openapi-fetch';
import type { paths } from './api/schema';

const api = createClient<paths>({ baseUrl: '/api' });

const { data, error } = await api.GET('/vacancies/{id}', {
  params: { path: { id: 42 } },
});
// data - точний тип відповіді 200, error - тип помилки з опису

Шлях, параметри й тип відповіді перевіряються компілятором: друкарська помилка в URL чи відсутній обов'язковий параметр - помилка TypeScript, а не 404 у продакшені.

Варіант 2 - повний SDK (OpenAPI Generator, Hey API, Orval, Kiota): згенеровані класи чи функції для кожної операції, моделі, інколи - готові хуки для TanStack Query.

Процес, що працює:

  1. специфікація - артефакт бекенду (Scramble scramble:export чи написана вручну) у репозиторії чи CI;
  2. генерація клієнта - крок збирання фронтенду або окремий пакет;
  3. зміна API ламає збирання фронтенду, якщо клієнтський код не відповідає новому контракту, - помилка виявляється до деплою.

Що варто врахувати:

  • якість згенерованого залежить від якості специфікації: type: object без властивостей дасть Record<string, unknown>, неописані помилки - відсутність типів для них. Генерація клієнтів швидко показує прогалини в документації;
  • nullable і необов'язкові поля - розрізняти «поле може бути null» і «поля може не бути» (required). Неточність тут - джерело помилок undefined на клієнті;
  • типи не перевіряють дані під час виконання. Відповідь сервера, що не відповідає контракту, тихо пройде. Для критичних даних - валідація схемою (Zod, згенерований зі специфікації);
  • не редагувати згенерований код вручну - зміни зникнуть при наступній генерації. Розширення - обгортками;
  • версії інструментів: не всі генератори повністю підтримують OpenAPI 3.1 (типи-масиви [string, 'null'], $ref поряд з іншими полями).

Альтернатива в межах Laravel + Inertia: Wayfinder генерує типізовані функції для маршрутів і дій контролерів без проміжної специфікації.

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

JSON:API - специфікація формату JSON-відповідей і правил запитів для API. Замість того, щоб кожна команда вигадувала власну структуру, вона фіксує готові рішення.

{
  "data": {
    "type": "posts",
    "id": "1",
    "attributes": { "title": "Laravel 13", "published_at": "2026-09-30T10:00:00Z" },
    "relationships": {
      "author": { "data": { "type": "users", "id": "7" } }
    }
  },
  "included": [
    { "type": "users", "id": "7", "attributes": { "name": "Оля" } }
  ]
}

Що стандартизує:

  • структура ресурсу: type, id (завжди рядок), attributes, relationships, links, meta;
  • include - пов'язані ресурси в масиві included, кожен один раз, навіть якщо на нього посилаються десятки записів;
  • sparse fieldsets - fields[posts]=title;
  • сортування, пагінація, фільтрація - назви параметрів (sort=-published_at, page[...], filter[...]);
  • формат помилок - масив errors з status, code, title, detail, source.pointer;
  • медіатип - application/vnd.api+json.

Laravel 13 має вбудовані JSON:API-ресурси:

php artisan make:resource PostResource --json-api
class PostResource extends JsonApiResource
{
    public $attributes = ['title', 'body', 'published_at'];

    public $relationships = ['author', 'comments'];
}

JsonApiResource формує структуру data/attributes/relationships, обробляє include і fields із запиту, серіалізує зв'язки лише коли клієнт їх запросив і виставляє правильний Content-Type. Глибину вкладених include обмежує JsonApiResource::maxRelationshipDepth(). Розбір фільтрів і сортування Laravel лишає за пакетами на кшталт spatie/laravel-query-builder.

Переваги стандарту:

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

Недоліки:

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

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

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

Коли контракт API погоджено (специфікація OpenAPI), фронтенд не повинен чекати на готовий бекенд. Мок-сервер відповідає за специфікацією.

Prism - мок-сервер, що читає OpenAPI:

npx @stoplight/prism-cli mock openapi.yaml
# слухає на http://127.0.0.1:4010
  • статичні відповіді з полів example/examples специфікації;
  • динамічні (--dynamic) - згенеровані дані, що відповідають схемам;
  • валідація запитів: запит з неправильним тілом чи без обов'язкового параметра отримає 422 - фронтенд одразу бачить, що надсилає не те;
  • вибір сценарію заголовком Prefer: code=404 чи Prefer: example=empty - перевірка обробки помилок;
  • режим проксі - перевіряє, що справжній сервер відповідає специфікації (контрольна точка між бекендом і контрактом).

MSW (Mock Service Worker) - моки на рівні клієнта:

import { http, HttpResponse } from 'msw';

export const handlers = [
  http.get('/api/vacancies/:id', ({ params }) =>
    HttpResponse.json({ data: { id: Number(params.id), title: 'Laravel Developer' } }),
  ),
  http.post('/api/applications', () =>
    HttpResponse.json({ errors: { email: ['Обов\'язкове поле'] } }, { status: 422 }),
  ),
];

Service Worker у браузері (чи перехоплювач у Node для тестів) відповідає на запити застосунку. Код застосунку не знає про моки - робить звичайні fetch.

Порівняння:

  • Prism - окремий сервер, що бере дані зі специфікації: моки не розходяться з контрактом;
  • MSW - моки в коді, повний контроль над сценаріями (затримки, помилки мережі, стан між запитами), спільні для розробки, тестів і Storybook. Але їх треба підтримувати вручну або генерувати зі специфікації.

Головний ризик моків - розходження з реальністю. Фронтенд «працює» з моками, а з реальним API - ні. Захист:

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

Для бекенду на Laravel аналогічна задача - Http::fake() для сторонніх API в тестах.

Докладніше в документації: Prism: мок-сервер для OpenAPI