Recently, one of the most trendy stacks is the JAMStack, which was popularized by Netlify. It’s mainly SPAs but committed to supporting statically generated sites which means no backend, a bit of JS that consumes APIs, and some HTML (I’m overly simplifying here).

Regular SPAs can have a backend that helps the application. For instance, the backend could do some server-side rendering (SSR) on your React/Vue components. They can also help out when it comes to consuming APIs. Usually, the communication is authenticated over OAuth2 but we can’t safely store OAuth client secrets in the browser. That’s the reasoning of a recent Laravel package called Sanctum — a lightweight, stateful (cookie-based) authentication mechanism for SPAs (it also provides stateless personal tokens).

The golden age of MVC

It hasn’t always been like that. We used to build full-stack web applications. Sam Stephenson from Basecamp has this great presentation about Turbolinks where he refers to this era as “The Golden Age of Web Development.”

Turbolinks is a bit controversial. Some people love it; others hate it. It doesn’t seem to share the same success rates as Rails itself. To my understanding, it works perfectly fine with Rails, but when I tried to use it outside of Rails, it’s a bit painful. That might be because Rails accommodates it really well, so everything seems to be set up to work nicely with Turbolinks (especially around forms and AJAX with UJS).

Regardless of your feelings about it, Turbolinks is what makes applications feel like an SPA, even though it’s just a traditional server-rendered application with sprinkles of JavaScript to make page transitions without a full-page refresh.

If you think that the more “traditional” ways of building web applications can’t compete in terms of speed with modern SPAs check out this presentation by Ben Halpern on how they made (a Rails app) so fast that it went viral in Japan.

With all that being said, Turbolinks do constrain us when it comes to frontend tooling. It makes some things easier, and others painful. If you want to get your React or Vue pages and components working with Turbolinks, you need to do some hacks because, as I said, pages don’t refresh. You need to manually kick your JS components on every Turbolinks load event so they can scan the page again.

The advent of mobile applications

The first iPhone was released in 2007 and this was the start of another era for web applications: the age of JSON APIs. It’s true that the very first version of the iPhone didn’t allow custom apps since the App Store was only released the following year, but I picked it just as a marker.

Mobile apps often need to talk to a backend. It does so by sending HTTP requests to your applications. However, these requests are more customizable than the ones sent by a typical web application. We are not limited to how browsers behave. We can send more nuanced requests using PUT, PATCH, and DELETE verbs, for instance. Since we are no longer limited to HTML forms, we can leverage other data formats like XML or JSON for both request and response bodies.

Together with that, JavaScript gained even more popularity and with the help of frameworks, we are able to build more powerful applications entirely in the browser, consuming the same endpoints our mobile applications use over AJAX.

It’s not always flowers and sunshine, though. We are not able to force smartphones to update the apps whenever we have a new version available, it’s up to the user to do that or have it configured to auto-update. In practice, this means that our APIs must be aware that old client versions are running in parallel for a while. It’s not just 1 version behind, it might be even older ones. It might also take days or weeks for clients to upgrade to a newer version.

We need versioning and that on itself brings a few challenges. We need to comply with data contracts and be very careful when removing and sometimes even adding new fields. To me, that’s one of the reasons GraphQL has gained so much attention recently. Clients can specify exactly what they need and how they need it in a graph format.

GraphQL isn’t that common in the PHP backend world, I should say. At least I don’t hear much about it. Maybe other languages like Elixir and Node have better support for it. Some other languages have their own preferred way to build web services, like Go and gRPC. gRPC and Protobuf solves the versioning problem quite well.

Do we really need to have separate applications at all? Is it really either a majestic monolith server-rendered HTML web applications or full SPAs+APIs? Despite what you might think, there is another way.

Inertia.JS: The return of MVC

The homepage for Inertia.JS states:

Inertia.js lets you quickly build modern single-page React, Vue and Svelte apps using classic server-side routing and controllers.

The idea behind it is that you don’t need to have separate teams communicating over JSON APIs, one working entirely in the frontend while the other works entirely in the backend. What if you can still have your convenient MVC applications and instead of HTML with Sprinkles of JavaScript, you replace your View layer entirely with JavaScript? That’s Inertia.

I thought about building an application to demonstrate it, but well, Jonathan Reinink and the Inertia community have already built several examples using Inertia. There is one in Laravel with React, with Vue, and with Svelte. There is another one in Rails with Vue as well. Oh, did I mention it can work in other languages/frameworks too? It has a protocol so, as long as the backend implements it, it’s good. I have even seen some folks talking about a Django version as well.

How it works

Your first request will be a full-page load. The subsequent requests are slightly different. Like Turbolinks, Inertia will send an AJAX request to the backend with an X-Inertia custom header. A middleware is responsible for taking an Inertia response from the controller and serializing it in a way so that it tells the frontend which page component should be rendered (or rerendered). That’s usually referred to as an Inertia Request.

Accept: text/html, application/xhtml+xml
X-Requested-With: XMLHttpRequest
X-Inertia: true
X-Inertia-Version: 6b16b94d7c51cbe5b1fa42aac98241d5

HTTP/1.1 200 OK
Content-Type: application/json

  "component": "Event",
  "props": {
    "event": {
      "id": 80,
      "title": "Birthday party",
      "start_date": "2019-06-02",
      "description": "Come out and celebrate Jonathan's 36th birthday party!"
  "url": "/events/80",
  "version": "c32b8e4965f418ad16eaebba1d4e960f"

Take a look at the example from the homepage:

class UsersController
    public function index()
        $users = User::active()
            ->only('id', 'name', 'email');

        return Inertia::render('Users', [
            'users' => $users

Instead of returning a view, we tell it to render a Users page component. What’s even cooler about this is that the data you pass down from your controller will be available as props in your page component:

import React from 'react';

const Users = ({ users }) => (
  /* ... */

export default Users;

The way Inertia frontend applications are structured is usually like this:

├── app.js
├── Pages
│   ├── Auth
│   ├── Contacts
│   ├── Dashboard
│   ├── Organizations
│   ├── Reports
│   └── Users
└── Components

And in your JS main file you have something like this:

import { InertiaApp } from '@inertiajs/inertia-react'
import React from 'react'
import { render } from 'react-dom'

const app = document.getElementById('app')

    resolveComponent={name => import(`./Pages/${name}`).then(module => module.default)}

See the import(`./Pages/${name}`)? The name variable is what you pass as the first argument of the Inertia::render method in your controller. From that point on, your application behaves just like an SPA: sending AJAX requests to the backend and (re)rendering JS components in the frontend. However, this time we don’t need an API.

Navigating with links

I told you that Inertia behaves like Turbolinks — well, that’s not actually true. Turbolinks hijacks all the links in your page and, for the ones pointing to your current domain, it sends AJAX requests to the backend and does a document.body.innerHTML = response (a bit more than that, actually, but that’s the idea). Inertia, however, will respect your links and have your full-page reloads.

To have your application behave like an SPA, you need to use a custom kind of link:

<InertiaLink href="/">Home</InertiaLink>

You can even have links that send other HTTP verbs (something you can’t in HTML, but there are techniques like Form Method Spoofing):

<InertiaLink href="/endpoint" method="post" data={{ foo: bar}}>Save</InertiaLink>

Check out the Links section for more.

Submitting forms

You also have to hijack your form submissions and make them using Inertia helpers:

import { Inertia } from '@inertiajs/inertia'
import React, { useState } from 'react'

export default function Edit() {
  const [values, setValues] = useState({
    first_name: "",
    last_name: "",
    email: "",

  function handleChange(e) {
    const key =;
    const value =
    setValues(values => ({
        [key]: value,

  function handleSubmit(e) {
    e.preventDefault()'/users', values)

  return (
    <form onSubmit={handleSubmit}>
      <label htmlFor="first_name">First name:</label>
      <input id="first_name" value={values.first_name} onChange={handleChange} />
      <label htmlFor="last_name">Last name:</label>
      <input id="last_name" value={values.last_name} onChange={handleChange} />
      <label htmlFor="email">Email:</label>
      <input id="email" value={} onChange={handleChange} />
      <button type="submit">Submit</button>

Since we are talking about forms, we can also easily hook in your regular form validation from the backend with the use of shared data.

Shared data

Some data can be shared between your page re-renders. Things like the authenticated user, the messages flashed into session before a redirect, and the errors from your last form submission are prime examples.

You need to configure the shared data somewhere in your app initialization, like the boot method in your AppServiceProvider. Then, you can specify which data should be merged with the components’ data.

use Illuminate\Support\Facades\Session;

    'errors' => function () {
        return Session::get('errors')
            ? Session::get('errors')->getBag('default')->getMessages()
            : (object) [];

In our page components, we can either accept the error bag as a prop or we can use the usePage hook (in the React version. This is particularly useful when you need to access app-level data in non-page components, like the currently authenticated user in your layout navigation component, for instance.):

const { errors } = usePage()

And in our JSX form, we can use it like this:

<label for="first_name">First name:</label>
<input id="first_name" value={values.first_name} onChange={handleChange} />
{errors.first_name && <div>{errors.first_name[0]}</div>}

The backend doesn’t change — you have your regular Laravel controllers:

class UsersController extends Controller
    public function index()
        return Inertia::render('Users/Index', [
          'users' => User::all(),

    public function store()
                'first_name' => ['required', 'max:50'],
                'last_name' => ['required', 'max:50'],
                'email' => ['required', 'max:50', 'email'],

        return Redirect::route('users');

Lazy evaluation and partial reloads

Since your controller will be invoked passing data down to your views, all the code in your controller will be executed and data will be passed down as props to the JS page component. Let’s say you are building a faceted search page, like Github:

When you click on the filters in the sidebar, you don’t need to recalculate them in the backend. Those faceted filters don’t change. The count of results doesn’t change when you click on them. They only change when the search term changes.

We can leverage lazy evaluation in our controller and only compute them during the first render or when the client says so:

class ExploreController extends Controller
    public function index()
        return Inertia::render('Explore/Index', [
            'countries' => function () {
                return Country::withCount('users')->get();
            // Evaluated immediately
            'users' => User::orderBy('name')
                ->select('id', 'name')
                // Only applied when there is a country code in the query string.
                ->when(request('country'), function ($query, $country) {
                   $query->where('country_code', $country);

Now, in our page component, we can tweak the countries filter to have links:

import React from 'react';

export default ({ users, countries }) => {
  const filterByCountry = (country) => (e) => {

    Inertia.visit(route('explore.index', { country: }), {
      only: ['users']

  return (
        { => (
            <a href="#" onClick={filterByCountry(country)}>
              Only in {} ({country.users_count})
      <UsersList users={users} />

This way we are telling Inertia to only evaluate the users key (if it was a lazily evaluated). Everything else will be ignored and old data will be used instead.

File uploads

Uploading a file to the backend requires a bit more code. The only difference is that instead of sending a JSON object to the method, you have to wrap the data as a JS FormData object:

const data = new FormData()
data.append('first_name', first_name || '')
data.append('last_name', last_name || '')
data.append('email', email || '')
data.append('password', password || '')
data.append('photo', photo || '')'/users', data)

Authentication and Authorization

For authentication, your application can leverage the solid and conventional way of authenticating web applications: sessions and cookies.

Authorization, however, needs a bit more work. See, when you are dealing with Blade templates, Laravel provides helper methods so you can construct your views taking authorization into account:

@can('update', $post)
    <!-- The Current User Can Update The Post -->
@elsecan('create', App\Post::class)
    <!-- The Current User Can Create New Post -->

@cannot('update', $post)
    <!-- The Current User Cannot Update The Post -->
@elsecannot('create', App\Post::class)
    <!-- The Current User Cannot Create A New Post -->

Since we are no longer using Blade, we don’t have those helpers in place. To make UIs aware of whether the current user has permission to perform actions in the application, we have to tell them. There are several ways you can do this. Here’s one example:

class UsersController extends Controller
    public function index()
        return Inertia::render('Users/Index', [
            'can' => [
                'create_user' => Auth::user()->can('users.create'),
            'users' => User::all()->map(function ($user) {
                return [
                    'first_name' => $user->first_name,
                    'last_name' => $user->last_name,
                    'email' => $user->email,
                    'can' => [
                        'edit_user' => Auth::user()->can('users.edit', $user),

We are adding an extra can object for each of our users, as well as passing down a top-level can object for more global authorization checks for this particular page. In your page component, you can toggle the actions the user can see based on these props:

export default ({ can, users }) => {
  return (
      <h1>Users page</h1>

      {can.create_users && (
        <a href={route('users.create')}>create user</a>

        { => (
          <li key={}>
            {user.can.edit_user && (
              <a href={route('users.edit', user)}>edit</a>

Note: you can share your backend routes with your JS components using a package like Ziggy.

Error handling

In an SPA application, if you want to debug errors you have to rely on things like the awesome Chrome DevTools and browser extensions that support your framework. It’s not a bad experience, but Inertia makes it a bit better. In the case of errors in your backend response, it shows you your error page in a modal (when in development mode), so you can have more information about what went wrong without actually having to open your DevTools.

It also works really well with the regular Vue/React DevTools, don’t worry. Laravel DebugBar also works as normal, updating on every Ajax request, so you can inspect things like how many queries are being executed and so on. You have the best of both worlds. Pretty amazing.

What about mobile?

We talked about mobile applications in the beginning and I have intentionally been avoiding it until now. Turbolinks has some adapters to make your responsive web applications feel like native applications by adding a tiny WebView wrapper. It will convert page transitions into activity transitions (on Android, or the equivalent in iOS). That’s actually the whole point of the talk from Sam that I linked above, and I have a short video demonstrating that here, if you are curious.

Inertia doesn’t have that. I wish I could add a “yet” here, but, to be honest, I don’t know if that’s on the radar. If you have to support the entire app feature set in a mobile form, you still need an API. However, I think that’s a pretty big “IF”. Most web applications don’t really need a mobile app, and when they do, you can create API endpoints specifically for the features you need to support. This way, you can have smaller (task-focused) native mobile applications consuming these endpoints while the bulk of your application is in a simple MVC application.

Or you can use Turbolinks itself and have server-rendered applications wrapped in WebViews that feel like a native app. The Basecamp app works like that. You can optimize for native UX and components per page, as needed.

Wrap up

I think Inertia.js provides a really good way to build JavaScript heavy web applications. It brings the goodies of your JavaScript framework of choice without the drawbacks of having to do an API or a GraphQL layer to feed it. The team is able to move way faster than doing API work, where you have to worry about API versioning, schema contracts, and so on.

I know there are patterns and tooling to deal with most problems related to API work (like versioning, for instance), but what if you can eliminate that problem entirely?

I’m also pretty pumped about the other unconventional ways of building web applications these days. Things like Livewire (and it’s older cousin LiveView) makes me really excited about the near future. Well, it’s already here, actually. I just haven’t used it in a production application. Yet.

Other useful technical guides