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/jsonLaravel повертає в JSON. Клієнтам API варто завжди надсилати цей заголовок - інакше помилка валідації може стати редиректом; - власний SPA на тому ж домені може ходити в
api.phpз сесійною автентифікацією Sanctum ($middleware->statefulApi()), без токенів; - маршрути з
web.phpтеж можуть віддавати JSON - для внутрішніх запитів Livewire/Inertia-застосунку окремий API часто не потрібен.
Перевірка: php artisan route:list --path=api показує всі маршрути API з middleware.
Повернути модель з контролера можна - 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 вирішує, як відповісти на виняток - 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) зможуть розпізнати токен, що потрапив у публічний репозиторій.
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()- захист від доступу до чужих записів.