Upgrade a Laravel 13 + Vue app from Inertia 2 to Inertia 3: the practical checklist

Inertia 2 stopped getting bug fixes on September 26, 2026. Here is the step-by-step upgrade we tested on a real Laravel 13 + Vue app: dependencies, the new Vite plugin, renamed APIs, Axios, SSR and a final audit.

For your AI agent

Copies a prompt. Paste it into the AI agent you already have open.

Read https://laravel-vuejs.space/upgrade-inertia-2-to-3.md
Inertia 2 stopped getting bug fixes on September 26, 2026. Here is the step-by-step upgrade we tested on a real Laravel 13 + Vue app: dependencies, the new Vite plugin, renamed APIs, Axios, SSR and a final audit.
Show the diff. Do not commit.

Next: Optimistic updates and useHttp in Inertia 3 with Vue: a like button and a live search

On this page · 14 sections
  1. Why upgrade now
  2. The checklist at a glance
  3. Step 1: upgrade the packages
  4. Step 2: audit removed and renamed APIs
  5. Handle errors without leaving the page
  6. Step 3: Axios, qs and lodash-es
  7. Step 4: the head attribute
  8. Step 5: adopt the Vite plugin
  9. Optional: default layouts
  10. Step 6: simplify SSR
  11. Step 7: test it
  12. What you get for your trouble
  13. Recap
  14. FAQ
  15. Next steps
  16. Sources

Your Laravel + Vue app still runs on Inertia 2, and since September 26, 2026 that version no longer gets bug fixes. Security fixes stop on March 26, 2027. Upgrade now, while it is a planned afternoon and not an emergency.

TL;DR

  • For a typical app the upgrade is small: after the package bump alone, our test app built, logged in, saved forms and passed all 40 feature tests.
  • Rename the removed APIs (router.cancel(), the invalid/exception events, Inertia::lazy()), and install Axios, qs or lodash-es yourself if you imported them.
  • Add the @inertiajs/vite plugin: resolve, setup and usually ssr.ts can go, and SSR runs in npm run dev.
  • Inertia 3 needs PHP 8.2+ and Laravel 11+.

What you'll learn

  • Which APIs were renamed or removed
  • What to do about Axios, qs and lodash-es
  • How the @inertiajs/vite plugin slims down app.ts and SSR

Who it's for: Laravel + Vue developers with an app on Inertia 2.x

Tested with: Laravel 13.35 · inertia-laravel 3.5 · @inertiajs/vue3 + @inertiajs/vite 3.9 · Vue 3.5 · Vite 8 · PHP 8.4

Why upgrade now#

Inertia 3 came out on March 26, 2026, and it is what the official Laravel Vue starter kit uses today. Inertia 2 is in security-only mode until March 2027 (see the support policy).

We tested every step below on the Laravel 13 Vue starter kit as it was just before the Inertia 3 release. The rest of the checklist is about removing old code and using what v3 adds.

The upgrade in four stages: bump the PHP and npm packages and republish config/inertia.php; fix renamed APIs, events, Axios/qs/lodash-es imports and the head attribute; adopt the @inertiajs/vite plugin for a slimmer app.ts and simpler SSR; then verify with php artisan test, vue-tsc and a browser smoke test.
The seven steps, grouped into four stages.

The checklist at a glance#

  1. Upgrade the packages and republish the config.
  2. Audit your code for removed and renamed APIs.
  3. Check your imports of Axios, qs or lodash-es.
  4. Rename the inertia head attribute (or switch to the new Blade components).
  5. Adopt the @inertiajs/vite plugin and slim down app.ts.
  6. Simplify SSR.
  7. Run your tests, then a browser smoke test.

Step 1: upgrade the packages#

bash
composer require inertiajs/inertia-laravel:^3.0 -W
npm install @inertiajs/vue3@^3.0 @inertiajs/vite@^3.0

@inertiajs/vite is optional, but you want it (step 5). Next, republish the config file, because v3 changed its structure. Then clear compiled views, because the output of the @inertia directive changed:

bash
php artisan vendor:publish --provider="Inertia\ServiceProvider" --force
php artisan view:clear

Gotcha: --force overwrites your config

--force replaces config/inertia.php. Commit first, then re-apply your changes afterwards.

The main change: page settings now live under a pages key, and the testing section is shorter.

config/inertia.phpphp
'pages' => [
    'ensure_pages_exist' => false,
    'paths' => [resource_path('js/pages')],
    'extensions' => ['js', 'jsx', 'svelte', 'ts', 'tsx', 'vue'],
],

'testing' => [
    'ensure_pages_exist' => true,
],

Check paths. The starter kits use lowercase js/pages, and older apps often use js/Pages.

Step 2: audit removed and renamed APIs#

Run this from your project root to find everything that needs a change:

bash
grep -rnE "router\.cancel\(|router\.on\('(invalid|exception)'|inertia:(invalid|exception)|hideProgress|revealProgress|future:|Inertia::lazy|LazyProp|require\(['\"]@inertiajs" resources/js app routes

Here is what each match means, from the official upgrade guide:

Inertia 2Inertia 3
router.on('invalid', …)router.on('httpException', …)
router.on('exception', …)router.on('networkError', …)
router.cancel() (sync visits only)router.cancelAll() (sync, async and prefetch)
hideProgress(), revealProgress()progress.hide(), progress.reveal()
defaults: { future: { … } }delete it: all four options are now always on
Inertia::lazy() / LazyPropInertia::optional()
require('@inertiajs/…')import: the packages are ESM only

To keep the old behavior of router.cancel(), call router.cancelAll({ async: false, prefetch: false }).

Handle errors without leaving the page#

The new per-visit callbacks let you deal with errors in place. Return false from onHttpException and Inertia will not show its error page:

Inertia 2, then Inertia 3:

resources/js/app.tstypescript
router.on('invalid', (event) => {
    event.preventDefault()
    toast.error('Server error. Please try again.')
})
router.on('exception', () => toast.error('You seem to be offline.'))
resources/js/pages/invoices/Create.vuetypescript
router.post('/invoices', data, {
    onHttpException: (response) => {
        toast.error(`Server error (${response.status}). Please try again.`)
        return false
    },
    onNetworkError: () => toast.error('You seem to be offline.'),
})

Tip: let TypeScript find the rest

router.cancel no longer exists in the v3 types, so vue-tsc --noEmit points to every place you still call it.

One quiet behavior change: useForm now keeps processing set to true until onFinish, not just until the response arrives. If you use form.processing to disable a submit button, the button now stays disabled a little longer, which is usually what you want.

Step 3: Axios, qs and lodash-es#

Inertia 3 no longer ships Axios. It uses a small built-in XHR client. For most apps nothing changes: Laravel’s CSRF protection keeps working, because the client reads the XSRF-TOKEN cookie and sends it in the X-XSRF-TOKEN header (we checked this with POST requests in our test app).

You do need to act in three cases.

You import axios, qs or lodash-es in your own code but never installed them yourself. They used to come in with Inertia, and now they don’t. Install them directly: npm install axios (or qs, or lodash-es).

You use Axios interceptors to add headers or log errors. Move them to Inertia’s built-in interceptors, which apply to the router, useForm, <Form> and useHttp:

resources/js/app.tstypescript
import { http } from '@inertiajs/vue3'

http.onRequest((config) => {
    config.headers['X-Tenant'] = currentTenant()
    return config
})

http.onError((error) => reportToSentry(error))

You really need Axios (a shared, customised instance, for example). Pass it through the adapter:

resources/js/app.tstypescript
import axios from 'axios'
import { axiosAdapter } from '@inertiajs/core'

createInertiaApp({
    http: axiosAdapter(axios.create({ timeout: 10_000 })),
})

Heads up: ES2022 only

The packages now target ES2022. If you must support very old browsers, add @vitejs/plugin-legacy.

Step 4: the head attribute#

v3 renamed the inertia attribute on head elements to data-inertia. In your root Blade view, <title inertia> becomes <title data-inertia>. In our test, Inertia still replaced the old title on the client. With SSR you can end up with duplicate tags, though, so rename it anyway.

Even better, use the new Blade components. The slot of <x-inertia::head> is only rendered when SSR is off, which finally fixes duplicate <title> tags:

resources/views/app.blade.phpdiff
 <head>
     @vite(['resources/css/app.css', 'resources/js/app.ts', "resources/js/pages/{$page['component']}.vue"])
-    <title inertia>{{ config('app.name', 'Laravel') }}</title>
+    <x-inertia::head>
+        <title>{{ config('app.name', 'Laravel') }}</title>
+    </x-inertia::head>
 </head>
 <body class="font-sans antialiased">
     <x-inertia::app />
 </body>

The @inertia and @inertiaHead directives still work if you would rather not touch this file.

Step 5: adopt the Vite plugin#

This is where your code gets shorter. Add the plugin to vite.config.ts, and remove the ssr entry from the Laravel plugin if you had one:

vite.config.tstypescript
import inertia from '@inertiajs/vite'
import laravel from 'laravel-vite-plugin'
import vue from '@vitejs/plugin-vue'
import { defineConfig } from 'vite'

export default defineConfig({
    plugins: [
        laravel({
            input: ['resources/css/app.css', 'resources/js/app.ts'],
            refresh: true,
        }),
        inertia(), 
        vue(),
    ],
})

The plugin resolves pages from ./pages or ./Pages and mounts the app for you, so the resolve and setup callbacks can go:

resources/js/app.tstypescript
import { createInertiaApp } from '@inertiajs/vue3'
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers'
import type { DefineComponent } from 'vue'
import { createApp, h } from 'vue'
import '../css/app.css'

createInertiaApp({
    title: (title) => (title ? `${title} - ${appName}` : appName),
    resolve: (name) =>
        resolvePageComponent(`./pages/${name}.vue`, import.meta.glob<DefineComponent>('./pages/**/*.vue')), 
    setup({ el, App, props, plugin }) { 
        createApp({ render: () => h(App, props) }).use(plugin).mount(el) 
    }, 
    progress: { color: '#4B5563' },
})

Do you register Vue plugins such as i18n or Pinia? Move them into withApp:

resources/js/app.tstypescript
createInertiaApp({
    withApp(app) {
        app.use(i18n).use(pinia)
    },
})

Tip: no rush on app.ts

Your old resolve and setup still work in v3. In our test, the app ran fine before we touched app.ts, so you can do this step later. The Laravel team made exactly this change in the starter kit (commit 290aba0).

Optional: default layouts#

The starter kit also moved page layouts out of each page and into one place. This is the callback in the Vue kit’s app.ts. Welcome has no layout. auth/ uses AuthLayout. settings/ nests SettingsLayout inside AppLayout. Everything else uses AppLayout.

resources/js/app.tstypescript
createInertiaApp({
    layout: (name) => {
        switch (true) {
            case name === 'Welcome':
                return null
            case name.startsWith('auth/'):
                return AuthLayout
            case name.startsWith('settings/'):
                return [AppLayout, SettingsLayout]
            default:
                return AppLayout
        }
    },
})

A page can then pass props to its layout with defineOptions({ layout: { breadcrumbs: [...] } }). That is optional, so do it page by page.

Step 6: simplify SSR#

With the Vite plugin, SSR runs in npm run dev. There is no separate SSR build and no inertia:start-ssr while you develop. The plugin also reuses app.ts for the server bundle, so you can usually delete resources/js/ssr.ts:

bash
git rm resources/js/ssr.ts

In production nothing new is needed: build both bundles, then start the SSR server (Node.js 22 or newer):

bash
npm run build:ssr   # vite build && vite build --ssr
php artisan inertia:start-ssr

We checked this in our test app. After deleting ssr.ts, vite build --ssr still produced bootstrap/ssr/app.js. The SSR server rendered the login page with exactly one <title>.

v3 can also turn SSR off for specific routes, via middleware or the facade. That is useful for pages that only make sense in the browser.

Step 7: test it#

Run the final audit

bash
php artisan test
npx vue-tsc --noEmit
npm run build

Then click through the paths that matter most: log in, submit a form with a validation error, upload a file, and open a page that uses deferred or partial reloads. If you have a global error handler, trigger a 500 to make sure your httpException listener runs.

What you get for your trouble#

Upgrading is more than staying supported. v3 adds features that remove real code from Vue apps:

FeatureWhat it replaces
useHttpHand-rolled processing/errors state around Axios calls for JSON requests that don’t change the page
Optimistic updates with automatic rollbackManual “update the UI, undo on error” code, for the router, useForm, <Form> and useHttp
Instant visits and layout propsWaiting on the server before showing the next page; prop drilling into layouts
Better SSR error messagesGuessing why the SSR server crashed

On Thursday we build two of those: an optimistic like button and a live search with useHttp. Follow the series so you don’t miss it.

Recap#

  • Inertia 2 is security-only until March 26, 2027; Inertia 3 needs PHP 8.2+ and Laravel 11+.
  • Bump the packages, republish config/inertia.php, and run view:clear.
  • Rename the removed APIs and events; install Axios, qs or lodash-es yourself if you used them.
  • Use data-inertia or <x-inertia::head> in the root view.
  • The @inertiajs/vite plugin removes resolve, setup and usually ssr.ts.

FAQ#

Do I have to rewrite app.ts to upgrade to Inertia 3?

No. The old resolve and setup callbacks still work in v3. Adopting the @inertiajs/vite plugin is optional, but it removes boilerplate and makes SSR run in npm run dev.

Does CSRF still work now that Inertia 3 dropped Axios?

Yes. The built-in client reads the XSRF-TOKEN cookie and sends it as the X-XSRF-TOKEN header, which is what Laravel expects.

What replaces router.cancel() in Inertia 3?

router.cancelAll(). To cancel only sync visits like before, call router.cancelAll({ async: false, prefetch: false }).

How long is Inertia 2 supported?

Bug fixes ended on September 26, 2026. Security fixes continue until March 26, 2027.

Can I still use Axios with Inertia 3?

Yes. Pass your instance through axiosAdapter from @inertiajs/core in the http option of createInertiaApp.

Next steps#

Sources#

Found this useful?

Ask questions, share what you built.

Join thousands of Laravel and Vue developers in our Facebook group, or get new posts in your Telegram feed. Sharing this article helps too.

Keep reading

Newsletter

Get the best of Laravel & Vue, once a month.

One short email with new guides, release notes worth reading and hand-picked community links. No spam, unsubscribe any time.