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: лінтинг і пошук змін, що ламають клієнтів.
Чого не робить жоден генератор: не придумує зрозумілих описів, бізнес-правил і сценаріїв використання - ці частини пишуться людьми.
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' для конкретних позицій.
Якщо 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.
Процес, що працює:
- специфікація - артефакт бекенду (Scramble
scramble:exportчи написана вручну) у репозиторії чи CI; - генерація клієнта - крок збирання фронтенду або окремий пакет;
- зміна API ламає збирання фронтенду, якщо клієнтський код не відповідає новому контракту, - помилка виявляється до деплою.
Що варто врахувати:
- якість згенерованого залежить від якості специфікації:
type: objectбез властивостей дастьRecord<string, unknown>, неописані помилки - відсутність типів для них. Генерація клієнтів швидко показує прогалини в документації; nullableі необов'язкові поля - розрізняти «поле може бутиnull» і «поля може не бути» (required). Неточність тут - джерело помилокundefinedна клієнті;- типи не перевіряють дані під час виконання. Відповідь сервера, що не відповідає контракту, тихо пройде. Для критичних даних - валідація схемою (Zod, згенерований зі специфікації);
- не редагувати згенерований код вручну - зміни зникнуть при наступній генерації. Розширення - обгортками;
- версії інструментів: не всі генератори повністю підтримують OpenAPI 3.1 (типи-масиви
[string, 'null'],$refпоряд з іншими полями).
Альтернатива в межах Laravel + Inertia: Wayfinder генерує типізовані функції для маршрутів і дій контролерів без проміжної специфікації.
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-бібліотеки.
Коли контракт 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 в тестах.