Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Inertia Laravel Laravel Package

inertiajs/inertia-laravel

The official Laravel adapter for Inertia.js. Build modern single-page apps using classic server-side routing and controllers—no API boilerplate. It serves JSON page props to Vue, React, or Svelte frontends while Laravel handles routing, middleware, and validation.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install the adapter and generate the middleware:

composer require inertiajs/inertia-laravel
php artisan inertia:middleware   # -> app/Http/Middleware/HandleInertiaRequests.php

2. Register it at the end of the web group so session, auth and ShareErrorsFromSession have already run.

Laravel 11+ (bootstrap/app.php):

->withMiddleware(function (Middleware $middleware) {
    $middleware->web(append: [
        \App\Http\Middleware\HandleInertiaRequests::class,
    ]);
})

Laravel 10 and below: add it to $middlewareGroups['web'] in app/Http/Kernel.php.

3. Create the root Blade template (resources/views/app.blade.php) — the single HTML shell the whole app boots from:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    @vite(['resources/js/app.js'])
    @inertiaHead   {{-- SSR head/meta injection; harmless without SSR --}}
</head>
<body>
    @inertia       {{-- or @inertia('app') to change the root element id --}}
</body>
</html>

4. First use case — replace one Blade view with a page component:

use Inertia\Inertia;

Route::get('/dashboard', fn () => Inertia::render('Dashboard', [
    'stats' => fn () => ['users' => User::count()],
]))->middleware('auth');

For prop-less pages, Route::inertia('/about', 'About') is the terse equivalent and still picks up all shared props.

Where to look first: vendor/inertiajs/inertia-laravel/src/Response.php (how props are resolved and filtered) and src/Middleware.php (shared props, version, empty-response handling). Then php artisan vendor:publish --tag=inertia-config and skim config/inertia.php — the testing.page_paths / page_extensions values are the ones you'll touch almost immediately, because starter kits ship resources/js/pages (lowercase) while the package default is resources/js/Pages.

Implementation Patterns

Controllers stay thin; props are the API. Inertia::render() accepts anything Laravel can serialize — arrays, Arrayable (Eloquent, API Resources), Responsable, and closures. Closures are your default for anything non-trivial, since they're only evaluated when actually needed:

return Inertia::render('Users/Index', [
    'filters' => $request->only('search', 'role'),
    'users'   => fn () => UserResource::collection(
        User::query()->filter($request->only('search', 'role'))->paginate(25)
    ),
]);

Keep the shape identical to what a JSON API would return — same Resource classes, same pagination envelope. That way the same endpoint can serve mobile clients via a wantsJson() branch if needed.

Pick the prop type per cost, not per habit. This is the single biggest day-to-day lever for perceived performance:

use Inertia\Inertia;

return Inertia::render('Dashboard', [
    // Always shipped on first load, cheapest.
    'user' => $request->user(),

    // Only when the client explicitly asks (router.reload({ only: ['activity'] }))
    'activity' => Inertia::optional(fn () => Activity::latest()->take(20)->get()),

    // Second round-trip automatically, after first paint
    'metrics' => Inertia::defer(fn () => Analytics::summary()),

    // Appended on the client instead of replacing (chat, feeds, logs)
    'messages' => Inertia::merge(fn () => $chat->messages()->latest()->paginate(50)),

    // Fetched once per client session, then trusted from cache
    'permissions' => Inertia::once(fn () => $user->allPermissions()),

    // Sent even when not requested (flash, toasts, version banners)
    'flash' => Inertia::always(fn () => session()->only(['success', 'error'])),

    // Infinite scroll with pagination metadata handled for you (v2.0.10+)
    'orders' => Inertia::scroll(fn () => $user->orders()->paginate(30)),
]);

Inertia::lazy() is deprecated in favour of Inertia::optional(); defer and scroll can be combined (v2.0.19), and flash data has first-class support in v2.0.16+ (Inertia::flash('success', 'Saved')).

Put anything global in HandleInertiaRequests::share() and wrap it in closures. This is evaluated on every Inertia request, so auth()->user() there means you never pass user from a controller again:

public function share(Request $request): array
{
    return [
        ...parent::share($request),
        'auth'  => fn () => ['user' => $request->user()?->only('id', 'name', 'email')],
        'flash' => fn () => $request->session()->only(['success', 'error']),
        'app'   => ['name' => config('app.name')],
    ];
}

For a single page, Inertia::share('breadcrumbs', [...]) inside the controller still works and merges last.

Let partial reloads drive the UX, not full page visits. Client-side:

router.reload({ only: ['activity'] })                 // tab switch
router.reload({ only: ['users.data', 'users.current_page'] }) // dot notation, v1.1+
router.reload({ except: ['metrics'] })

Server-side filtering is automatic — you write no code for this. This is how you build tabs, filters and "load more" without ever touching a separate API endpoint. Combine with Inertia::optional() so the initial payload stays small.

Validation and redirects need no special handling. $request->validate(), FormRequest and ->withErrors() all work; errors are shared automatically under the errors prop, honouring X-Inertia-Error-Bag for named bags on multi-form pages. For non-GET Inertia requests, redirects are converted to 303 See Other so the browser follows with a GET — expect that status code in tests. Returning nothing from a controller redirects back (v0.6+), and you can override that via onEmptyResponse() on the middleware.

Testing is a first-class citizen — use it as your prop contract check:

use Inertia\Testing\AssertableInertia as Assert;

$this->get('/users')
    ->assertInertia(fn (Assert $page) => $page
        ->component('Users/Index')
        ->url('/users')
        ->has('users.data', 25)
        ->where('users.data.0.name', 'Ada')
        ->missing('users.data.0.password')
        ->etc()   // don't fail on unasserted props
    );

Testing the conditional branches means sending the Inertia partial headers yourself:

$this->get('/users', [
    'X-Inertia' => 'true',
    'X-Inertia-Partial-Component' => 'Users/Index',
    'X-Inertia-Partial-Data' => 'activity',
])->assertInertia(fn (Assert $page) => $page->has('activity', 20));

Recent versions also ship assertInertiaFlash / assertInertiaFlashMissing macros and helpers for deferred props.

SSR is a flag, not a rewrite. Publish the config, run php artisan inertia:start-ssr (add --runtime=bun if you prefer), and inertia:stop-ssr in deploy scripts. SSR auto-enables when bootstrap/ssr/ssr.js exists; pin it explicitly with inertia.ssr.enabled in CI. Keep @inertiaHead in the root template or you'll get SSR-rendered body with no <title>.

Gotchas and Tips

  • Middleware order is the #1 "props are empty" bug. HandleInertiaRequests must run after the session and auth middleware. If shared props or errors are missing, check the group order before touching the frontend.
  • Always delegate version() to the parent (return parent::version($request);) unless you intentionally hand-roll it. The default reads the Vite manifest hash; overriding it with a static value causes either stale assets or a permanent 409 X-Inertia-Version mismatch loop where every click triggers a full page reload.
  • Never cache an Inertia HTML response without Vary: X-Inertia. The same URL returns HTML for a fresh visit and JSON for an Inertia visit. The package sets Vary: X-Inertia for you (v0.6.2+), but a CDN rule, Cache::remember on the response, or a full-page cache module can silently strip it and serve the JSON payload or a stale page to the wrong client.
  • assertInertia is strict about props. Every prop must be asserted or you hit "unexpected properties"; ->etc() is the normal fix, not a smell. It also verifies the page component exists on disk unless you pass false as the second argument — that's usually the real cause of a failing component() assertion, and the fix is config/inertia.php → testing.page_paths / page_extensions matching your actual directory casing.
  • Deferred, optional and once props are invisible to naive assertions. $page->has('metrics') fails on a fresh request because those props simply aren't in the payload. Either request them with the partial headers above, or use the deferred-prop test helper.
  • optional() ≠ always(). optional() (formerly lazy()) is only sent when explicitly requested in only. always() is always sent, even when not requested. Reaching for the wrong one gives you a prop that either never appears or bloats every response. On the client, an optional prop you never request is undefined forever, and a defer prop is undefined until its second request lands — so components must handle that state rather than assuming arrays.
  • once() has cache semantics you must test for. Once props are trusted from the client after the first load; if the underlying value changes you need the client to request it fresh, and onceProps metadata in the response is what drives that (v2.0.13, and v2.0.14 fixed loaded once props from leaking into deferred metadata). Don't use once() for anything that changes per-request like counters or "last visited".
  • Closures are not evaluated eagerly, so lazy-loading bugs hide. fn () => $user->posts inside a deferred prop can trigger N+1 the moment the client asks for it — with no eager loading applied, because the relationship loads in a different code path. Eager-load inside the closure (User::with('posts')->find(...)), and remember closures must return serializable data; a raw model with hidden attributes will leak them if $hidden isn't set.
  • Keep APP_DEBUG=false in anything user-facing. Under an X-Inertia request, a 500 returns a non-Inertia error response and the client surfaces it in an error modal — with debug on, that modal contains your stack trace and env.
  • Prefer Inertia::location($url) over redirect()->away() for full-page navigations (OAuth, file downloads, external checkout). It returns a 409 with X-Inertia-Location for Inertia visits and a plain redirect otherwise, which is exactly the behaviour browsers need.
  • SSR failures degrade silently by design. A crashed SSR server yields a null response (handled since v0.5.1) and the app falls back to client rendering — so a broken SSR bundle can go unnoticed in staging. Watch logs, and remember SSR code runs in Node: no window, no document, and any hydration mismatch will show up as flicker rather than an exception.
  • Extension points worth knowing before you build workarounds: override share(), version() and onEmptyResponse() in your middleware; use Inertia::share() for request-scoped globals; add the history-encryption middleware / Inertia::encryptHistory() if you need URL-visible page state hidden; and use $request->inertia() (IDE-helper macro, v1.1+) rather than sniffing X-Inertia headers by hand. Persistent props (Inertia::persistent(), v1.2+) are the escape hatch when a middleware-shared prop must survive partial reloads.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo