Разработка · 6 мин чтения

Laravel Inertia: SPA без отдельного API

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(). Переключать подход можно на уровне отдельного роута.

Есть задача?

Обсудим в мессенджере

Расскажите, что нужно сделать — отвечу в течение 4 часов в рабочее время. Первая консультация бесплатно.

Продолжая пользование настоящим сайтом Вы выражаете своё согласие на обработку Ваших персональных данных (файлов куки) с использованием Yandex.Metrika.
Понятно