Introducing InertiaJS into Laravel for a nice Vue integration process
After a decade in Node.js land, I found myself preparing for an interview requiring Laravel and InertiaJS knowledge.
This is what I learned in the process pre,during and post interview on what i could gather was a way to build a simple product management system, without the need of a REST API using Vue directly within the laravel application.
Enter InertiaJS, as their landing page states:
Build single-page apps, without building an API.
Create modern single-page React, Vue, and Svelte apps using classic server-side routing. Works with any backend — tuned for Laravel.
As a JS Dev, this sounds really great but there's still a hurdle to fence off, PHP + Laravel.
I've followed Laravel's development and history because I considered it the superior framework in PHP, this without actually implementing anything not even in my homelab for years (10+), early this year I started assisting a team with a migration from PHP 5 (Hello OOP and PDO! Greetings from 2024!) to Laravel and even though the initial bit of the project wasn't done by me, the code architecture is so seamless when it comes to barebones Laravel that I thought I was ready for implementing InertiaJS into it in the interview process.
My friends, I was wrong!
But, let's cut the yapping about myself. I was taught how to give people value for their time, so here it is.
After this read of blog series, you should either have a better understanding on how to implement this (InertiaJS + Vue into Laravel) and remember it forever or you will have this bookmarked as it is going to become your reference, at least until some other cool kid pops up as a new solution that will make this even easier to attain!
Tech stack Prerequisites 🛠️:
You will need to have the following already installed in your environment, if you are like me, go ahead and deploy a docker container with ubuntu and install these things so you don't pollute your local:
- PHP 8.1+
- Composer 2.x
- Node.js 20 >= Come on, we are old enough, refrain from NodeJS 16, let the thing rest already
- Basic understanding of Laravel and Vue, I will leave some links at the bottom for these two tech bits in case you are not fluent in it.
What You'll Build 🎯
A simple backend where you will have a nice table to list products, filter them through a search box, order them and if necessary, delete them.
Things like Server-side validation, a basic authentication system and reports will also come into play and more importantly, an SPA experience without any of the REST API complexity handling.
Setting Up the Foundation 🏗️
# Create a new Laravel project composer create-project laravel/laravel inertia-products-project cd inertia-products-project # Install Inertia.js and Vue adapter composer require inertiajs/inertia-laravel php artisan inertia:middleware # Setup certain frontend features with Breeze (includes Vue + Inertia), this will assist with a basic boilerplate for this integration, authentication batteries included! composer require laravel/breeze --dev php artisan breeze:install vue
After running these commands, you'll have a fresh Laravel installation, Inertia.js middleware configured, VueJS 3 with Vite setup, Authentication scaffolding and Tailwind CSS (for the win!) for styling.
Lets run the app, shall we?
# You will need to have two different terminal sessions for this, you can use multiple tabs, multiple terminal sessions in VSCode/IntelliJ/WebStorm/Cursor or multiplex that with tmux. # Laravel server php artisan serve # Vite development server npm install && npm run dev
Visit http://localhost:8000 or the port you defined at your .env
file to verify your installation in case you changed the default values. You should see Laravel's welcome page with Vue components, thelogin and register buttons/links at the top right of the screen loading successfully.
Creating Our Product Backend 🔧
First, let's create our model and migration:
# Generate model, migration, controller and factory php artisan make:model Product -mf php artisan make:controller ProductController --resource
Update the migration file:
// database/migrations/[timestamp]_create_products_table.php public function up(): void { Schema::create('products', function (Blueprint $table) { $table->id(); $table->string('name'); $table->text('description'); $table->integer('available_stock'); $table->decimal('price', 10, 2); $table->timestamps(); }); }
Configure the Product model:
// app/Models/Product.php namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class Product extends Model { use HasFactory; // We will be going with name, description, available_stock and price initially protected $fillable = [ 'name', 'description', 'available_stock', 'price' ]; protected $casts = [ 'price' => 'decimal:2' ]; }
Create factory for testing data:
// database/factories/ProductFactory.php public function definition(): array { return [ 'name' => fake()->text(25), // Use either this or generate a list of names and pick it randomly here with generic PHP 'description' => fake()->paragraph(), 'available_stock' => fake()->numberBetween(0, 100), 'price' => fake()->randomFloat(2, 10, 1000) ]; }
Update the ProductController:
// app/Http/Controllers/ProductController.php namespace App\Http\Controllers; use App\Models\Product; use Illuminate\Http\Request; use Inertia\Inertia; class ProductController extends Controller { public function index() { return Inertia::render('Products/Index', [ 'products' => Product::latest()->get() ]); } public function store(Request $request) { $validated = $request->validate([ 'name' => 'required|max:255', 'description' => 'required', 'available_stock' => 'required|integer|min:0', 'price' => 'required|numeric|min:0' ]); Product::create($validated); return redirect()->route('products.index') ->with('message', 'Product created successfully'); } }
Add routes:
// routes/web.php Route::middleware(['auth', 'verified'])->group(function () { Route::get('/products', [ProductController::class, 'index'])->name('products.index'); Route::post('/products', [ProductController::class, 'store'])->name('products.store'); });
Run migrations:
php artisan migrate
Optional: Seed database with test data:
# Run this directly in your terminal to give you access to the Laravel REPL directly for your terminal, in case you don't know what a REPL is, feel free to visit this link https://www.digitalocean.com/community/tutorials/what-is-repl php artisan tinker # Once then, run the two commands below. # Making a call with "use" here is a really important command, should you forget it, it will give you a big error saying that Product class cannot be found along the lines of: # ❌ Error: Class "Product" not found use App\Models\Product; Product::factory()->count(10)->create();
This way, we now have the products table with the temporary required fields, a Product
model with proper attributes and casting of possible pesky value types (I see you floats), a factory for testing, the ProductController
for validation and basic CRUD operations and most importantly basic CRUD.
Building the Frontend with Vue 3 and Inertia ✨
After setting up our very basic backend, let's create the UI bit for managing products. We'll use Vue 3's Composition API and Inertia.js to create this seamless single-page experience.
Creating the Product Management Interface
First, create the Products directory in your Pages folder:
mkdir -p resources/js/Pages/Products
Now with the directory in place, lets create the main product management component at path resources/js/Pages/Products/Index.vue
, it will include a form to add additional products with real-time validation feedback, error and success messages or alerts and a table to display all our juicy definitely-not fictional products
<script setup> import { useForm } from '@inertiajs/vue3' import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue' import { Head } from '@inertiajs/vue3' const props = defineProps({ products: Array, flash: Object }) const form = useForm({ name: '', description: '', available_stock: '', price: '' }) const submit = () => { form.post(route('products.store'), { onSuccess: () => form.reset() }) } </script> <template> <Head title="Products" /> <AuthenticatedLayout> <template #header> <h2 class="font-semibold text-xl text-gray-800 dark:text-gray-600 leading-tight"> Products Management </h2> </template> <div class="py-12"> <div class="max-w-7xl mx-auto sm:px-6 lg:px-8"> <!-- Form Section --> <div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm p-6 mb-6"> <h3 class="text-lg font-medium mb-4 text-gray-800 dark:text-gray-400">Add New Product</h3> <div v-if="flash?.message" class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-4"> {{ flash.message }} </div> <form @submit.prevent="submit" class="space-y-6"> <div class="grid grid-cols-1 md:grid-cols-2 gap-6"> <div> <label class="block text-sm font-medium text-gray-700 dark:text-gray-200">Name</label> <!-- Make it so this input field has validation --> <input v-model="form.name" type="text" class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 dark:bg-gray-900 text-white dark:text-gray-200" @blur="form.name = form.name.trim(); if (!form.name) {form.errors.name = 'Product name is required.'} else {form.errors.name = ''}" /> <div v-if="form.errors.name" class="text-red-500 text-sm mt-1">{{ form.errors.name }}</div> </div> <div> <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Price</label> <input v-model="form.price" type="number" step="1.0" class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 dark:bg-gray-900 text-white dark:text-gray-200" @blur="form.price = form.price; if (!form.price) {form.errors.price = 'Please add a value to the product of at least 1.'} else {form.errors.price = ''}" /> <div v-if="form.errors.price" class="text-red-500 text-sm mt-1">{{ form.errors.price }}</div> </div> </div> <div> <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Description</label> <textarea v-model="form.description" rows="2" class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 dark:bg-gray-900 text-white dark:text-gray-200" @blur="form.description = form.description.trim(); if (!form.description) {form.errors.description = 'Please add a description to your product.'} else {form.errors.description = ''}" ></textarea> <div v-if="form.errors.description" class="text-red-500 text-sm mt-1">{{ form.errors.description }}</div> </div> <div> <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Available Stock</label> <input v-model="form.available_stock" type="number" step="1" class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 dark:bg-gray-900 text-white dark:text-gray-200" @blur="form.available_stock = form.available_stock; if (!form.available_stock) {form.errors.available_stock = 'Please add any available stock to your product.'} else {form.errors.available_stock = ''}" /> <div v-if="form.errors.available_stock" class="text-red-500 text-sm mt-1">{{ form.errors.available_stock }}</div> </div> <button type="submit" :disabled="form.processing" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" > Add Product </button> </form> </div> <!-- Products Table --> <div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm"> <div class="p-6"> <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700"> <thead> <tr> <th class="px-6 py-3 bg-gray-50 dark:bg-gray-700 text-left"> <span class="text-xs font-medium text-gray-500 dark:text-gray-300 uppercase">Name</span> </th> <th class="px-6 py-3 bg-gray-50 dark:bg-gray-700 text-left"> <span class="text-xs font-medium text-gray-500 dark:text-gray-300 uppercase">Stock</span> </th> <th class="px-6 py-3 bg-gray-50 dark:bg-gray-700 text-left"> <span class="text-xs font-medium text-gray-500 dark:text-gray-300 uppercase">Price</span> </th> </tr> </thead> <tbody class="bg-white divide-y divide-gray-200 dark:divide-gray-700 dark:bg-gray-800"> <tr v-for="product in products" :key="product.id"> <td class="px-6 py-4 whitespace-nowrap text-white dark:text-gray-200">{{ product.name }}</td> <td class="px-6 py-4 whitespace-nowrap text-white dark:text-gray-200">{{ product.available_stock }}</td> <td class="px-6 py-4 whitespace-nowrap text-white dark:text-gray-200">${{ product.price }}</td> </tr> </tbody> </table> </div> </div> </div> </div> </AuthenticatedLayout> </template>
Implementing this will give you a few nice features, things like form validation (both at the moment of input data into each field and when submitting), input fields to insert your items and a lovely table to show your newly created products!
To test it out, just go ahead and spin up the server, remember, npm run dev and php artisan serve in two different sessions and visit your server at localhost:8000/products to check your newly created component.
Common Errors & Solutions 🚨
Class Not Found in Tinker
❌ Error: Class "Product" not found ✅ Solution: use App\Models\Product
Route Not Found
❌ Error: Route [products.index] not defined ✅ Solution: Check web.php and run php artisan route:list, check that your route is in place and properly configured.
Jose, this works great and i could adapt this to my use-case if I ever need this, but what's next? How do controllers work? How do i implement InertiaJS into an existing project to have my back and front ends of my application at the same place?
I wanted to give you a place to start with a working app, albeit being really crude visually-wise, it can store data into your database of choice (Laravel's default is a SQLite database), next chapter coming in next week we will go over more details of Laravel, Vue and InertiaJS for this kind of solution and how to leverage everything to bend it to our will.
Once you made it this far, I wanted to thank you for spending this time with this solution making blog post series and for your willingness to learn something new!
What's next? 🎯
- Implementing the search capability
- Sorting by all columns
- Build a report out of the list of available products
- Deploying your application
Reference Links 📚
Laravel Basics: link Vue 3 Guide: link InertiaJS Docs (Everything is in a single page): link
Have a great day and stay awesome.