Laravel как бэкенд для SPA и мобилки: что важно не запороть

Laravel очень удобно ставить за React/Vue или мобильным приложением. Но если на старте чуть-чуть схалтурить с архитектурой, дальше всё превращается в «монолит с кучей костылей».

Расскажу, что я делаю в начале проекта, чтобы API не развалилось через полгода.

1. Сразу решаю: это API, а не «обычный сайт»

На уровне соглашений: • контроллеры пишут JSON-ответы, а не view(); • фронт сам решает, как рисовать страницы; • всё, что касается HTML, лежит отдельно (если вообще нужно).

Минимальный набор в routes/api.php:

Route::middleware('auth:sanctum')->group(function () { Route::get('/me', [UserController::class, 'me']); Route::apiResource('products', ProductController::class); });

И отдельный routes/web.php — только под админку/лендинг.

2. Контракты API и версии

Я сразу фиксирую: • базовый URL, например /api/v1; • формат ошибок; • как приходят/уходят даты, цены, статусы.

Пример префикса:

Route::prefix('v1')->group(function () { // все текущие эндпоинты });

Если потом понадобится v2, старых клиентов можно не ронять.

3. Контроллеры: тонкие, без бизнес-логики

Контроллер только: • принимает Request, • валидирует, • вызывает сервис/Action, • возвращает Resource.

Пример:

public function store(StoreProductRequest $request) { $product = CreateProduct::run($request->validated());

return new ProductResource($product); }

Где: • StoreProductRequest — вся валидация; • CreateProduct — бизнес-логика; • ProductResource — форма ответа.

Такой подход проще поддерживать и тестировать.

4. Валидация через FormRequest

FormRequest сразу даёт: • нормальный список правил; • единый формат ошибок.

class StoreProductRequest extends FormRequest { public function rules(): array { return [ 'name' => ['required', 'string', 'max:255'], 'price' => ['required', 'numeric', 'min:0'], 'sku' => ['nullable', 'string', 'max:64'], ]; } }

Ошибки frontend получает как 422 Unprocessable Entity с полями.

5. Resources: не светим структуру базы наружу

Я почти всегда использую JsonResource:

class ProductResource extends JsonResource { public function toArray($request): array { return [ 'id' => $this->id, 'name' => $this->name, 'price' => (float) $this->price, 'status'=> $this->status, ]; } }

Плюсы: • можно переименовать поля; • скрывать внутренние детали; • добавлять вычисляемые значения без ломки API.

6. Работа с БД: миграции, связи, индексы

На старте: • продумываю связи hasMany, belongsToMany; • сразу ставлю индексы под ожидаемые фильтры; • избегаю «свалки» в одной таблице.

Пример миграции:

Schema::create('products', function (Blueprint $table) { $table->id(); $table->string('name'); $table->decimal('price', 10, 2)->index(); $table->string('sku')->nullable()->unique(); $table->timestamps(); });

Важно не забыть index() там, где потом будем фильтровать/сортировать.

7. Аутентификация и права

Для SPA/мобилки я чаще всего: • использую Sanctum для токенов; • разделяю роли через Gate и полиси.

Пример полиси:

public function update(User $user, Product $product): bool { return $user->is_admin; }

Фронт не должен гадать, что ему можно — бэкенд честно отвечает 403.

8. Чек-лист старта проекта на Laravel как API • Есть чёткое разделение web.php (HTML) и api.php (JSON). • Все эндпоинты идут через /api/v1. • Используются FormRequest-классы для валидации. • Логика вынесена в сервисы/Actions, контроллеры тонкие. • Ответы формируются через JsonResource, структура БД не торчит наружу. • Миграции с индексами под ключевые фильтры. • Настроена аутентификация (Sanctum/Passport) и полиси по ролям.

Итог

Laravel очень комфортен как бэкенд для SPA и мобильных приложений, но комфорт быстро исчезает, если сваливать всё в контроллеры и напрямую светить модели наружу.