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

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

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

100 питань

  • POST - створити ресурс (або виконати дію). Сервер сам обирає адресу нового ресурсу: POST /orders → 201 Created з Location: /orders/42.
  • PUT - замінити ресурс цілком тим, що в тілі запиту. Поля, яких немає в тілі, вважаються видаленими чи скинутими.
  • PATCH - частково змінити ресурс: лише передані поля.
PUT /users/42
{"name": "Оля", "email": "olia@example.com", "phone": null}

PATCH /users/42
{"phone": "+380501234567"}

Ідемпотентність - ключова відмінність, про яку питають:

  • PUT і DELETE ідемпотентні: повторний однаковий запит лишає ресурс у тому самому стані. Їх можна безпечно повторити при обриві з'єднання.
  • POST не ідемпотентний: повтор створить друге замовлення.
  • PATCH - залежить від змісту: «встановити phone» ідемпотентний, «збільшити лічильник на 1» - ні.
  • GET, HEAD, OPTIONS - ще й безпечні: не змінюють стан узагалі.

На практиці:

  • Багато API використовують лише PATCH для оновлень, бо клієнту рідко потрібно надсилати весь ресурс.
  • Для створення з повторами (платежі) POST роблять ідемпотентним через заголовок Idempotency-Key.
  • Дії, що не вкладаються в CRUD (/orders/42/cancel), зазвичай оформлюють як POST на під-ресурс.

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

Успіх (2xx):

  • 200 OK - звичайна успішна відповідь з тілом.
  • 201 Created - ресурс створено; бажано з заголовком Location.
  • 202 Accepted - запит прийнято, обробка асинхронна (поставлено в чергу).
  • 204 No Content - успіх без тіла: видалення, оновлення без повернення даних.

Помилки клієнта (4xx):

  • 400 Bad Request - запит некоректний: зламаний JSON, неправильний формат.
  • 401 Unauthorized - не автентифіковано: немає токена чи він недійсний. Попри назву, це про «хто ви?».
  • 403 Forbidden - автентифіковано, але немає прав на цю дію.
  • 404 Not Found - ресурсу немає (або ви не маєте права знати, що він існує).
  • 409 Conflict - конфлікт стану: дублікат, застаріла версія при оптимістичному блокуванні.
  • 422 Unprocessable Content - синтаксис правильний, але дані не пройшли валідацію. Laravel так і відповідає на помилки валідації.
  • 429 Too Many Requests - перевищено ліміт запитів, з Retry-After.

Помилки сервера (5xx): 500 - непередбачена помилка; 502/503/504 - проблеми з upstream, перевантаження, обслуговування.

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

  • 200 OK з {"success": false, "error": "..."} - клієнти, проксі й моніторинг вважатимуть запит успішним.
  • 500 на помилку валідації - це не помилка сервера.
  • 403 для чужого ресурсу інколи розкриває, що він існує; тоді свідомо віддають 404.

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

REST (Representational State Transfer) - архітектурний стиль, описаний Роєм Філдінгом. Це не протокол і не формат, а набір обмежень:

  • ресурси й ідентифікатори: усе, з чим працює API, - ресурси з власними URL (/orders/42), а не «процедури»;
  • уніфікований інтерфейс: дії виражаються стандартними HTTP-методами (GET, POST, PUT, PATCH, DELETE) з їхньою семантикою;
  • представлення: клієнт отримує не сам ресурс, а його представлення (JSON, XML) - залежно від заголовка Accept;
  • без стану (stateless): кожен запит містить усе потрібне для обробки (зокрема автентифікацію). Сервер не пам'ятає «сесію розмови» між запитами;
  • кешованість: відповіді позначають, чи можна їх кешувати;
  • багаторівнева система: між клієнтом і сервером можуть бути проксі, CDN, балансувальники - і клієнт про це не знає.

«Просто JSON через HTTP» (стиль RPC) виглядає інакше:

POST /api/getOrder         {"id": 42}
POST /api/cancelOrder      {"id": 42}
POST /api/updateOrderStatus

RESTful:

GET    /api/orders/42
PATCH  /api/orders/42      {"status": "cancelled"}
DELETE /api/orders/42

Що дає REST на практиці:

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

Реальність: більшість «REST API» не виконують усіх обмежень (зокрема гіпермедіа - HATEOAS), і це нормально. На співбесіді важливо розуміти суть - ресурси, семантика методів і кодів, відсутність стану, - а не догматичність.

RPC-стиль не заборонений: для дій, що не вкладаються в CRUD, чи для внутрішніх сервісів RPC (зокрема gRPC) інколи природніший. Головне - послідовність у межах одного API.

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

Безпечний метод не змінює стан на сервері - лише читає. Ідемпотентний метод - повторний виклик з тими самими даними дає той самий стан сервера, що й одиночний.

Метод Безпечний Ідемпотентний
GET, HEAD, OPTIONS так так
PUT ні так
DELETE ні так
POST ні ні
PATCH ні не гарантовано

Приклади:

  • PUT /users/7 {"name": "Оля"} - один раз чи п'ять, результат той самий: ім'я «Оля»;
  • DELETE /orders/42 - перший виклик видаляє, наступні нічого не змінюють (відповідь може бути 404, але стан однаковий);
  • POST /orders - кожен виклик створює нове замовлення;
  • PATCH {"balance": {"increment": 100}} - не ідемпотентний, а PATCH {"status": "paid"} - фактично ідемпотентний. Тому про PATCH кажуть «не гарантовано».

Ідемпотентність стосується стану, а не відповіді. Відповіді можуть відрізнятися (200 і потім 404 для DELETE), і updated_at може змінитися - важливо, що ефект для клієнта той самий.

Чому це важливо:

  • повтори при збоях мережі. Клієнт не отримав відповіді - він не знає, чи запит виконався. Ідемпотентний запит можна безпечно повторити. Неідемпотентний - ні: повтор POST /payments може списати гроші двічі. Для таких операцій потрібен ключ ідемпотентності;
  • проксі, браузери, бібліотеки автоматично повторюють і попередньо завантажують безпечні запити. Якщо GET /logout чи GET /orders/42/delete змінює стан, прийде «невидимий» користувач - пошуковий робот, попереднє завантаження посилань - і виконає дію;
  • кешування: відповіді на безпечні методи можна кешувати.

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

  • зміна стану в GET (лічильники, «відмітити прочитаним», видалення за посиланням);
  • PUT, реалізований як «додати до наявного» (тоді він не ідемпотентний);
  • POST для читання з великим тілом запиту - допустимо як виняток (складний пошук), але відповідь не кешується, і повтор не очевидно безпечний.

У Laravel CSRF-захист і так не перевіряє GET/HEAD/OPTIONS - ще одна причина не змінювати стан у них.

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

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

1. Стиль ключів - один на весь API: snake_case (природний для Laravel і баз даних) або camelCase (природний для JavaScript). Головне - не змішувати: created_at в одній відповіді й updatedAt в іншій.

2. Дати й час - ISO 8601 / RFC 3339 з часовим поясом:

{ "created_at": "2026-10-04T10:15:00+03:00", "paid_at": "2026-10-04T07:15:00Z" }

Не "04.10.2026", не мітка часу без пояснення, не локальний час без зміщення. «Дата без часу» (день народження) - окремий формат "2026-10-04".

3. Гроші - не числа з рухомою комою: мінімальні одиниці цілим числом ("amount": 125050 копійок) або рядок ("125.50") плюс явна валюта.

4. Ідентифікатори - рядки, якщо можуть бути великими: числа понад 2^53 JavaScript округлює. Для BIGINT чи Snowflake-ID безпечніше "id": "9007199254740993".

5. null чи відсутнє поле: вирішити й задокументувати. Поширений підхід - поле завжди є, null означає «немає значення». Відсутність поля - лише для свідомо необов'язкових чи прихованих правами.

6. Обгортка відповіді: { "data": ..., "meta": ..., "links": ... } для колекцій (пагінація в meta) - так роблять ресурси Laravel. Обгортка дає місце для метаданих без зміни структури даних.

7. Енуми - рядки, а не числа: "status": "paid" читається й не ламається при зміні порядку значень.

8. Булеві значення - true/false, а не 1/0 чи "yes".

9. Помилки - єдиний формат для всіх ендпойнтів (наприклад, Problem Details).

10. Без «магічних» значень: -1 замість null, порожній рядок замість відсутності.

Технічні деталі JSON:

  • кодування - UTF-8 (вимога RFC 8259 для обміну між системами);
  • порядок ключів в об'єкті не гарантується - клієнт не повинен на нього покладатися;
  • дублікати ключів - невизначена поведінка, різні парсери обирають різне значення.

У Laravel: касти дат у моделях ('paid_at' => 'datetime') серіалізуються в ISO 8601 UTC, API Resources керують ключами й форматом явно.

Докладніше в документації: RFC 8259: формат JSON

  • Автентифікація відповідає на питання «хто ви?»: перевіряє токен, ключ, пароль.
  • Авторизація - «що вам можна?»: чи має цей користувач право на цю дію з цим ресурсом.

Спершу автентифікація, потім авторизація. Помилки - різні: 401 - не вдалося встановити, хто ви; 403 - відомо, хто ви, але дія заборонена.

Способи автентифікації в API:

  • API-ключ у заголовку (X-API-Key чи Authorization: Bearer ...) - для серверних інтеграцій.
  • Токени користувачів - персональні токени доступу (Laravel Sanctum) чи токени OAuth 2.0.
  • Cookie-сесія - для власного SPA на тому самому домені (Sanctum SPA-режим), з CSRF-захистом.
  • JWT - самодостатній підписаний токен; сервер перевіряє підпис без запиту до бази, але відкликати такий токен до закінчення терміну складніше.

Авторизація - на кожному ендпоінті, на сервері:

public function update(UpdateOrderRequest $request, Order $order)
{
    $this->authorize('update', $order);   // чи це замовлення цього користувача
    // ...
}

Найпоширеніша вразливість API (OWASP API Top 10, BOLA) - є автентифікація, але немає перевірки прав на конкретний об'єкт: GET /orders/43 повертає чуже замовлення, бо код перевірив лише, що користувач залогінений.

Ще правила: токени лише по HTTPS, ніколи в URL (потраплять у логи й історію браузера), з обмеженим терміном дії й мінімально потрібними правами (scopes).

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

Браузер дотримується same-origin policy: JavaScript зі сторінки https://shop.example не може прочитати відповідь з https://api.other.example. CORS (Cross-Origin Resource Sharing) - механізм, яким сервер дозволяє браузеру послабити це обмеження для певних джерел.

Access-Control-Allow-Origin: https://shop.example
Access-Control-Allow-Methods: GET, POST, PATCH
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Allow-Credentials: true

Для «непростих» запитів (з Authorization, JSON-тілом, методами PUT/DELETE) браузер спершу надсилає preflight - OPTIONS-запит з питанням, чи можна. Access-Control-Max-Age дозволяє кешувати відповідь preflight.

Чому CORS - не захист API:

  • Його виконує браузер. curl, Postman, скрипт на сервері, мобільний застосунок CORS не перевіряють і отримують відповідь незалежно від заголовків.
  • CORS захищає користувача в браузері від того, щоб чужий сайт від його імені читав дані з вашого API. Але не захищає сам API від прямих запитів.
  • Навіть із забороненим CORS простий запит (POST з формою) доходить до сервера й виконується - браузер лише не віддає скрипту відповідь. Тому від CSRF рятують токени й cookie SameSite, а не CORS.

Захист API - це автентифікація, авторизація, ліміти запитів, валідація.

Типові помилки налаштування:

  • Access-Control-Allow-Origin: * разом з credentials - браузер це не дозволяє.
  • Віддзеркалення будь-якого Origin з запиту у відповідь разом з Allow-Credentials: true - фактично дозволяє будь-якому сайту читати дані користувача.

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

Усі три способи відповідають на питання «хто робить запит», але для різних клієнтів.

Сесійна cookie - браузерна автентифікація:

  • користувач входить, сервер створює сесію і ставить cookie (HttpOnly, Secure, SameSite);
  • браузер сам додає cookie до кожного запиту;
  • для кого: власний фронтенд на тому ж домені (Blade, Livewire, Inertia, SPA через Sanctum);
  • переваги: токен недоступний JavaScript (захист від крадіжки через XSS), вихід - знищити сесію на сервері;
  • потребує захисту від CSRF - бо браузер надсилає cookie автоматично.

Токен доступу (bearer token) - автентифікація від імені користувача для небраузерних клієнтів:

Authorization: Bearer 1|AbCdEf...
  • для кого: мобільні застосунки, десктоп, CLI, сторонні клієнти від імені користувача (OAuth);
  • токени можна обмежити правами (scopes/abilities) і терміном дії, відкликати окремо для кожного пристрою;
  • CSRF не загрожує - браузер сам токен не додає.

API-ключ - ідентифікація застосунку-інтегратора, а не людини:

  • для кого: сервер-сервер інтеграції (партнер вивантажує замовлення, CRM синхронізує контакти);
  • зазвичай довгоживучий, прив'язаний до облікового запису клієнта, з власними лімітами й правами;
  • передається в заголовку (Authorization чи X-Api-Key), ніколи в URL.

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

  • токени в localStorage для власного SPA - будь-який XSS їх вкраде. Для SPA на своєму домені cookie-сесія безпечніша;
  • API-ключ у мобільному застосунку чи фронтенді - будь-хто дістане його з бандлу. Ключі - лише на серверах;
  • токени без терміну дії і без можливості відкликання;
  • ключі в Git, логах, URL - сканери публічних репозиторіїв знаходять їх за хвилини.

Як зберігати на сервері: як паролі - лише хеш (порівняння з хешем отриманого значення). Тоді витік бази не дає готових ключів. Так зберігає токени Laravel Sanctum (SHA-256).

Найпоширеніша вразливість API за OWASP - не спосіб автентифікації, а відсутність перевірки доступу до конкретного об'єкта після автентифікації.

Докладніше в документації: OWASP API Security Top 10 (2023)

Через HTTP без шифрування кожен запит видно будь-кому на шляху: публічний Wi-Fi, провайдер, скомпрометований роутер. Токени в Authorization, cookie сесій, паролі в тілі запиту, персональні дані у відповідях - усе в відкритому вигляді. Крім читання, трафік можна змінити (вставити скрипт, підмінити відповідь).

HTTPS (TLS) дає:

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

Правила для API:

  • лише HTTPS, без «а на HTTP теж працює». Запит на HTTP краще відхиляти, а не перенаправляти: редирект 301 для API означає, що токен уже пройшов відкритим каналом у першому запиті;
  • сертифікати - автоматичне оновлення (Let's Encrypt, Caddy, хмарні балансувальники), моніторинг терміну дії;
  • сучасний TLS (1.2+, краще 1.3), без застарілих шифрів.

HSTS (Strict-Transport-Security) - заголовок, що каже браузеру: «з цим доменом - лише HTTPS, протягом указаного часу»:

Strict-Transport-Security: max-age=31536000; includeSubDomains

Після першого візиту браузер сам переписуватиме http:// на https:// ще до відправки запиту - атака з «пониженням» до HTTP (SSL stripping) не спрацює. preload і включення домену в список браузерів захищає навіть перший візит.

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

  • HSTS стосується браузерів. Мобільні застосунки й серверні клієнти його не читають - їх треба конфігурувати на HTTPS-адреси;
  • includeSubDomains - лише якщо всі піддомени готові до HTTPS, інакше вони стануть недоступними на весь max-age;
  • за проксі (Cloudflare, балансувальник) TLS може закінчуватися на проксі. Laravel має довіряти заголовкам X-Forwarded-Proto (trustProxies), інакше генеруватиме http:// посилання й вважатиме запити незахищеними;
  • cookie з Secure не передаються по HTTP взагалі.

Між внутрішніми сервісами теж варто шифрувати трафік: «внутрішня мережа» часто не така закрита, як здається, а для критичних з'єднань - взаємна автентифікація (mTLS).

Докладніше в документації: Strict-Transport-Security

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

// небезпечно
$user->update($request->all());
PATCH /api/profile
{"name": "Оля", "is_admin": true, "balance": 1000000}

Дзеркальна проблема - зайві дані у відповіді: return $user; віддає всі атрибути моделі, включно з тими, що клієнту бачити не можна (внутрішні прапорці, хеші, токени, приховані поля інших користувачів). OWASP об'єднує обидві проблеми в категорію «порушена авторизація на рівні властивостей об'єкта».

Захист на вході:

// лише перевірені поля
$validated = $request->validate([
    'name' => ['required', 'string', 'max:255'],
    'bio' => ['nullable', 'string', 'max:1000'],
]);
$user->update($validated);
  • validate() / Form Request повертає лише ті поля, для яких є правила, - решта ігнорується;
  • $fillable у моделі - друга лінія захисту: навіть update($request->all()) не запише поля поза списком. $guarded = [] вимикає цей захист;
  • поля, залежні від прав (роль, статус модерації), - окремі ендпойнти чи явні перевірки: «змінювати role може лише адміністратор».

Захист на виході:

return new UserResource($user);   // явний перелік полів
  • API Resource чи DTO з явним переліком полів замість серіалізації моделі;
  • $hidden у моделі (паролі, токени) - корисно, але недостатньо: краще відповідь описувати явно;
  • поля за правами: 'email' => $this->when($request->user()->can('viewContacts', $this->resource), $this->email).

Ознаки проблеми під час рев'ю коду:

  • $request->all() чи $request->input() без валідації, передане в create/update;
  • return $model або $model->toArray() у контролерах API;
  • $guarded = [] у моделях, що змінюються через API.

Тест, що варто мати: відправити зайве поле (is_admin: true) і перевірити, що воно не змінилося.

Докладніше в документації: OWASP API3:2023 - авторизація на рівні властивостей

У свіжому застосунку Laravel файлу routes/api.php немає - його додає команда:

php artisan install:api

Вона встановлює Laravel Sanctum (автентифікація токенами), створює routes/api.php і підключає його в bootstrap/app.php:

->withRouting(
    web: __DIR__.'/../routes/web.php',
    api: __DIR__.'/../routes/api.php',
    // apiPrefix: 'api/v1',
)

Чим маршрути API відрізняються:

web.php api.php
префікс URL немає /api (змінюється через apiPrefix)
група middleware web api
сесія й cookie так ні (stateless)
CSRF-захист так ні
автентифікація сесія токени (auth:sanctum)
// routes/api.php
Route::middleware('auth:sanctum')->group(function () {
    Route::get('/user', fn (Request $request) => $request->user());
    Route::apiResource('posts', PostController::class);
});

apiResource реєструє маршрути ресурсу без create і edit (вони потрібні лише для HTML-форм): index, store, show, update, destroy. Контролер - php artisan make:controller PostController --api --model=Post.

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

  • група api за замовчуванням не обмежує частоту запитів. Обмеження вмикається явно: визначити лімітер RateLimiter::for('api', ...) і підключити $middleware->throttleApi() в bootstrap/app.php (або throttle:api на маршрутах);
  • відповіді-помилки для запитів з Accept: application/json Laravel повертає в JSON. Клієнтам API варто завжди надсилати цей заголовок - інакше помилка валідації може стати редиректом;
  • власний SPA на тому ж домені може ходити в api.php з сесійною автентифікацією Sanctum ($middleware->statefulApi()), без токенів;
  • маршрути з web.php теж можуть віддавати JSON - для внутрішніх запитів Livewire/Inertia-застосунку окремий API часто не потрібен.

Перевірка: php artisan route:list --path=api показує всі маршрути API з middleware.

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

Повернути модель з контролера можна - Laravel серіалізує її в JSON автоматично:

return $user;   // усі атрибути моделі (крім $hidden)

Але так формат відповіді API = структура таблиці. Додали колонку - вона з'явилася в API. Перейменували - зламали клієнтів. Внутрішнє поле (прапорець, службова дата) - уже публічне.

API Resource - окремий шар, що явно описує, як модель виглядає назовні:

php artisan make:resource UserResource
class UserResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'avatar_url' => $this->avatarUrl(),
            'registered_at' => $this->created_at,
            'posts_count' => $this->whenCounted('posts'),
            'email' => $this->when($request->user()?->is($this->resource), $this->email),
        ];
    }
}
return new UserResource($user);
return UserResource::collection($users);
return $user->toResource();          // те саме, коротше

Що дає ресурс:

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

Обгортка data: відповідь має вигляд { "data": { ... } }. Вимкнути - JsonResource::withoutWrapping() у сервіс-провайдері, але для колекцій з пагінацією обгортка корисна (там же meta).

JSON:API. Якщо потрібен стандартний формат специфікації JSON:API (type, id, attributes, relationships, included), у Laravel 13 є вбудований JsonApiResource - php artisan make:resource PostResource --json-api.

Пастка: ресурс не захищає від N+1. Якщо в toArray звертатися до незавантажених зв'язків ($this->author->name), кожен елемент колекції - окремий запит. Звідси whenLoaded і with() у контролері.

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

Laravel вирішує, як відповісти на виняток - HTML-сторінкою чи JSON, - за заголовком Accept запиту ($request->expectsJson()).

Помилка валідації для запиту з Accept: application/json:

HTTP/1.1 422 Unprocessable Content

{
  "message": "The email field is required. (and 1 more error)",
  "errors": {
    "email": ["The email field is required."],
    "password": ["The password field must be at least 8 characters."]
  }
}

Без Accept: application/json та сама помилка валідації - це редирект назад (302) з помилками в сесії, як для HTML-форм. Клієнт API отримає HTML сторінки замість зрозумілої помилки. Найчастіша причина «API повертає 302 замість 422».

Інші типові відповіді:

Ситуація Статус
немає чи недійсний токен (AuthenticationException) 401 {"message": "Unauthenticated."}
authorize() / політика відмовила 403
модель не знайдено (findOrFail, прив'язка маршруту) 404
перевищено ліміт (throttle) 429 з Retry-After
виняток у коді 500

APP_DEBUG=true додає до відповіді 500 повідомлення винятку, файл, рядок і стек. На продакшені - обов'язково false, інакше API розкриває внутрішню будову коду.

Примусово JSON для всіх маршрутів API - незалежно від заголовка клієнта:

// bootstrap/app.php
->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->shouldRenderJsonWhen(
        fn (Request $request, Throwable $e) => $request->is('api/*') || $request->expectsJson(),
    );
})

Власний формат для конкретного винятку:

$exceptions->render(function (OrderAlreadyShippedException $e, Request $request) {
    return response()->json(['message' => 'Замовлення вже відправлено'], 409);
});

Для клієнтів API варто задокументувати: завжди надсилати Accept: application/json, а формат помилки - єдиний для всіх ендпойнтів.

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

Laravel Sanctum - легка автентифікація для API: персональні токени доступу (мобільні застосунки, інтеграції) і сесійна автентифікація для SPA.

Модель користувача:

use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens;
}

Видача токена (наприклад, для входу з мобільного застосунку):

Route::post('/tokens', function (Request $request) {
    $request->validate([
        'email' => ['required', 'email'],
        'password' => ['required'],
        'device_name' => ['required', 'string', 'max:255'],
    ]);

    $user = User::where('email', $request->email)->first();

    if (! $user || ! Hash::check($request->password, $user->password)) {
        throw ValidationException::withMessages(['email' => ['Невірні облікові дані.']]);
    }

    return ['token' => $user->createToken($request->device_name)->plainTextToken];
});

Токен має вигляд 5|xYz...: id запису й випадкова частина.

Використання - заголовок Authorization:

GET /api/user
Authorization: Bearer 5|xYz...
Accept: application/json
Route::get('/user', fn (Request $request) => $request->user())->middleware('auth:sanctum');

Як Sanctum зберігає токени: у таблиці personal_access_tokens лежить лише SHA-256-хеш випадкової частини. Відкритий токен показується один раз при створенні - потім його неможливо відновити, лише створити новий. Витік бази не дає готових токенів.

Відкликання:

$request->user()->currentAccessToken()->delete();   // вихід з цього пристрою
$user->tokens()->delete();                            // вихід з усіх пристроїв

Термін дії: за замовчуванням токени не мають терміну дії. Його задають глобально (expiration у config/sanctum.php, хвилини) або для конкретного токена третім аргументом createToken. Прострочені записи прибирає sanctum:prune-expired у планувальнику.

Що варто зробити в продакшені:

  • обмежити частоту запитів до ендпойнта видачі токенів (захист від перебору паролів);
  • задати термін дії токенів;
  • device_name зрозумілий користувачу - щоб він міг побачити список пристроїв і відкликати зайвий;
  • префікс токенів (token_prefix у конфігурації) - сканери секретів (наприклад, GitHub) зможуть розпізнати токен, що потрапив у публічний репозиторій.

Докладніше в документації: Laravel Sanctum: API-токени

Form Request - окремий клас для валідації й авторизації запиту. Контролер отримує вже перевірені дані.

php artisan make:request StoreOrderRequest
class StoreOrderRequest extends FormRequest
{
    public function authorize(): bool
    {
        return $this->user()->can('create', Order::class);
    }

    public function rules(): array
    {
        return [
            'items' => ['required', 'array', 'min:1', 'max:50'],
            'items.*.product_id' => ['required', 'integer', Rule::exists('products', 'id')->where('active', true)],
            'items.*.qty' => ['required', 'integer', 'between:1,100'],
            'comment' => ['nullable', 'string', 'max:1000'],
        ];
    }
}
public function store(StoreOrderRequest $request): JsonResponse
{
    $order = $this->orders->create($request->user(), $request->validated());

    return (new OrderResource($order))->response()->setStatusCode(201);
}

Що відбувається автоматично:

  • Laravel створює запит і викликає authorize() до контролера. false - відповідь 403;
  • rules() - валідація; помилки - 422 з полем errors (для запитів з Accept: application/json);
  • контролер виконується лише якщо все пройшло.

Чому це краще за $request->validate() у контролері:

  • контролер коротший і читається як бізнес-логіка;
  • правила й авторизацію легко перевикористати (створення й оновлення часто ділять більшість правил);
  • $request->validated() - лише перевірені поля. Передавати їх у create() безпечно: зайве поле з тіла запиту (is_admin) туди не потрапить.

Корисні можливості:

  • prepareForValidation() - нормалізувати вхідні дані до перевірки (обрізати пробіли, привести телефон до одного формату);
  • after() - перевірки, що охоплюють кілька полів або потребують бази;
  • messages() і attributes() - власні тексти помилок і назви полів;
  • $stopOnFirstFailure - зупинити валідацію на першій помилці.

Пастки API:

  • межі масивів (max:50) і рядків обов'язкові: клієнт може надіслати мегабайти даних;
  • exists з умовами (where('active', true)) - інакше можна замовити неактивний чи чужий товар;
  • sometimes для PATCH: поле перевіряється, лише якщо прийшло, - часткове оновлення не вимагає всіх полів;
  • авторизація конкретного об'єкта ($this->route('order')) в authorize() - захист від доступу до чужих записів.

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

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

Рівні
Junior 35 Middle 35 Senior 30

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