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

Junior: питання на співбесіді з теми «API у Laravel»

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

5 питань

У свіжому застосунку 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