Laravel Inertia беру каждый раз, когда нужен SPA-интерфейс на Vue или React, но городить отдельный REST API и разворачивать фронтенд отдельным проектом смысла нет. Inertia.js склеивает серверный роутинг Laravel с клиентскими компонентами: контроллер отдаёт не JSON для чужого API, а готовую страницу с пропсами, а браузер обновляется без перезагрузки, как в обычном SPA. На своих проектах на Laravel я так собирал админки и личные кабинеты, в том числе со связками с CRM и приёмом оплаты через T‑Bank, и ни разу не пожалел, что не стал поднимать отдельный бэкенд под API.
Что такое Inertia.js и как она работает с Laravel
Inertia не фреймворк и не замена Vue или React, это тонкий клей между Laravel и SPA-компонентами. Контроллер вызывает Inertia::render() вместо view(), передаёт туда массив данных как пропсы, а на клиенте Inertia подставляет их в нужный Vue- или React-компонент. Роутинг остаётся серверным: маршруты описаны в routes/web.php, как в обычном Laravel-приложении, никакого параллельного роутера на фронте нет.
При переходе между страницами Inertia не перезагружает документ целиком, а делает XHR-запрос, получает JSON с пропсами нужной страницы и подменяет компонент через клиентский роутинг, отсюда ощущение SPA. При этом в базе, контроллерах и валидации ничего не меняется по сравнению с обычным Laravel-проектом на Blade, поэтому команда, которая уже писала классические веб-приложения на Laravel, осваивает Inertia за пару дней.
Установка и базовая настройка SPA на Vue или React
Со стороны Laravel ставится один пакет:
composer require inertiajs/inertia-laravel
php artisan inertia:middleware
Вторая команда создаёт класс App\Http\Middleware\HandleInertiaRequests, его нужно зарегистрировать в цепочке web-мидлваров. В Laravel 11 и новее это делается в bootstrap/app.php:
->withMiddleware(function (Middleware $middleware) {
$middleware->web(append: [
\App\Http\Middleware\HandleInertiaRequests::class,
]);
})
На фронте ставится JS-адаптер под выбранный фреймворк, например для Vue:
npm install @inertiajs/vue3
И описывается точка входа в resources/js/app.js:
import { createApp, h } from 'vue'
import { createInertiaApp } from '@inertiajs/vue3'
createInertiaApp({
resolve: name => {
const pages = import.meta.glob('./Pages/**/*.vue')
return pages[`./Pages/${name}.vue`]()
},
setup({ el, App, props, plugin }) {
createApp({ render: () => h(App, props) })
.use(plugin)
.mount(el)
},
})
Корневой Blade-шаблон остаётся один на всё приложение, обычно resources/views/app.blade.php:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
@vite('resources/js/app.js')
@inertiaHead
</head>
<body>
@inertia
</body>
</html>
Все компоненты-страницы лежат в resources/js/Pages, имя файла привязано к строке, которую контроллер передаёт в Inertia::render().
Роуты и контроллеры без отдельного REST API
В Inertia-подходе роуты остаются в routes/web.php, никакого параллельного routes/api.php под фронт заводить не нужно:
Route::get('/users', function () {
return Inertia::render('Users/Index', [
'users' => User::select('id', 'name', 'email')->paginate(20),
]);
});
Contoller отдаёт Eloquent-коллекцию прямо в пропсы, без промежуточного слоя сериализации под чужого потребителя API. Если нужно скрыть часть полей или посчитать производные значения, я всё равно оборачиваю модель в Laravel API Resource, но использую его как обычный трансформер данных, а не как публичный контракт эндпоинта:
return Inertia::render('Users/Index', [
'users' => UserResource::collection(User::paginate(20)),
]);
Общие для всех страниц данные, например текущего пользователя или флеш-сообщения, я кладу в метод share() мидлвара HandleInertiaRequests, чтобы не дублировать их в каждом контроллере:
public function share(Request $request): array
{
return array_merge(parent::share($request), [
'auth' => [
'user' => $request->user(),
],
'flash' => [
'success' => fn () => $request->session()->get('success'),
],
]);
}
Бесплатный материал
🎁 Полезный скрипт в подарок
Подпишитесь на Telegram - пришлю готовый скрипт по этой теме.
Без спама. Отписка в 1 клик.
Формы и валидация: как Laravel и Inertia обмениваются данными
Валидация в Inertia-подходе остаётся полностью серверной, как в обычном Laravel-приложении. Контроллер валидирует запрос через $request->validate(), а при ошибке Laravel делает редирект назад с флеш-данными в сессии:
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users,email',
]);
User::create($validated);
return redirect()->route('users.index');
}
На клиенте Inertia сама подхватывает ошибки валидации из сессии и раскладывает их по объекту form.errors, дополнительного кода под обработку 422-ответа писать не нужно:
<script setup>
import { useForm } from '@inertiajs/vue3'
const form = useForm({
name: '',
email: '',
})
function submit() {
form.post('/users')
}
</script>
<template>
<form @submit.prevent="submit">
<input v-model="form.name" type="text">
<div v-if="form.errors.name">{{ form.errors.name }}</div>
<input v-model="form.email" type="email">
<div v-if="form.errors.email">{{ form.errors.email }}</div>
<button :disabled="form.processing">Сохранить</button>
</form>
</template>
CSRF-токен Inertia подставляет в запросы автоматически через сессионные куки Laravel, отдельно настраивать заголовки под каждый запрос не приходится, в отличие от классической связки Laravel Sanctum с вынесенным на отдельный домен фронтендом.
Частичные перезагрузки и ленивые пропсы
Когда страница тяжёлая, например таблица с фильтрами и статистикой, полный ре-рендер всех пропсов на каждый клик по фильтру расточителен. Inertia умеет запрашивать только часть пропсов:
router.get('/users', { search: 'Иван' }, { only: ['users'] })
Лишние пропсы контроллер даже не начнёт вычислять, если обернуть их в Inertia::optional(), такой пропс посчитается только когда его явно запросили через only:
return Inertia::render('Users/Index', [
'users' => User::paginate(20),
'stats' => Inertia::optional(fn () => $this->calculateStats()),
]);
На практике это снимает большую часть жалоб на «SPA тормозит из-за лишних запросов»: тяжёлая агрегация по статистике не гоняется при каждом клике по пагинации таблицы.
Аутентификация и SSR: что доступно из коробки
Под Inertia у Laravel есть готовые стартовые наборы (Breeze и Jetstream умеют работать в Inertia-режиме), которые сразу разворачивают логин, регистрацию, сброс пароля и middleware auth на нужных роутах, без написания собственных эндпоинтов авторизации.
Server-side рендеринг у Inertia тоже есть, и отдельный пакет для него не нужен: в разработке SSR берёт на себя Vite-плагин, а для прода бандл собирается командой npm run build, после чего Node-процесс поднимается командой php artisan inertia:start-ssr. Он рендерит первую страницу на сервере ради SEO и скорости первого показа. Но это отдельный процесс поверх PHP-хостинга: ему нужен рабочий Node.js, его придётся держать запущенным, перезапускать при деплое и следить, что он не упал, отдельно от веб-сервера, который отдаёт сам Laravel. Если хостинг чисто PHP-based и Node на нём не поднять, SSR просто не подключаете, само SPA-приложение при этом продолжает работать через обычный клиентский рендеринг.
Когда Inertia не подходит
Inertia отлично закрывает внутренние SPA: админки, личные кабинеты, CRM-модули, где фронтенд и бэкенд пишет одна команда и деплоятся вместе. Но у подхода есть естественный предел.
| Критерий | Inertia.js | Отдельный REST API + SPA |
|---|---|---|
| Нужен ли отдельный API-слой | Нет, контроллеры Laravel отдают пропсы напрямую | Да, JSON-контракт с версионированием |
| Мобильное приложение как отдельный клиент | Не подходит, пропсы заточены под конкретную SPA-страницу | Подходит, один API кормит и веб, и мобильный клиент |
| Независимый деплой фронта и бэка | Нет, фронт собирается вместе с Laravel-проектом | Да, фронтенд можно выкатывать отдельно |
| Скорость старта для внутренней админки | Высокая, минимум лишнего кода | Ниже, нужно поддерживать два проекта |
Если на проекте уже запланировано мобильное приложение или сторонние интеграции, которым нужен именно JSON-контракт, я закладываю классический REST API поверх Laravel Sanctum сразу, а не пытаюсь потом выпиливать Inertia-пропсы в отдельный API. Когда таких требований нет, а вся команда работает в одном Laravel-репозитории, я обычно беру заказ на разработку бэкенда на Laravel именно с Inertia, это экономит время на стыковке фронта с API, которого просто не существует как отдельного слоя.
Laravel регулярно обновляется, поэтому перед внедрением я сверяюсь с актуальной документацией.
Частые вопросы
Нужен ли Laravel Sanctum, если я работаю через Inertia
Для самой Inertia-связки Sanctum не нужен, аутентификация держится на обычных сессионных куках Laravel, как в Blade-приложении. Sanctum подключают отдельно, когда поверх того же Laravel-проекта нужно ещё и токен-based API для мобильного приложения или внешних интеграций.
Можно ли использовать React вместо Vue с Inertia и Laravel
Да, у Inertia есть официальные адаптеры под Vue, React и Svelte, серверная часть на Laravel при этом не меняется вообще, меняется только пакет на фронте и синтаксис компонентов в папке Pages.
Как быть с SEO, если Inertia рендерит страницы на клиенте
Без SSR поисковые боты получают пустой контейнер, который заполняется JS, что не подходит публичным лендингам и каталогам. Для внутренних админок и личных кабинетов, куда поисковики не заходят, это не проблема. Если SEO критичен, подключают SSR-режим Inertia с отдельным Node-процессом либо для конкретно этой страницы не используют Inertia, а отдают её обычным Blade-шаблоном.
Что делать, если часть страниц сайта должна остаться на Blade
Inertia и Blade прекрасно живут в одном проекте: страницы, которым нужен интерактивный SPA-интерфейс, отдаются через Inertia::render(), а обычные статические или редко обновляемые страницы остаются на классических Blade-вьюхах через view(). Переключать подход можно на уровне отдельного роута.