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 и мобильных приложений, но комфорт быстро исчезает, если сваливать всё в контроллеры и напрямую светить модели наружу.