Laravel 12 Inertia.js + Vue.js CRUD Tutorial

Laravel 12 Inertia.js + Vue.js CRUD Tutorial

 Laravel 12 Inertia.js + Vue.js CRUD Tutorial (for Beginners)

In this comprehensive guide, you'll learn how to build a Post CRUD (Create, Read, Update, Delete) application using Laravel 12, Inertia.js, and Vue.js 3. This stack lets you enjoy Laravel’s powerful backend with the reactive user experience of Vue, without building a full-blown SPA or using APIs.

Requirements

Before we start, ensure the following are installed:

  • PHP 8.2+

  • Composer

  • Node.js and npm

  • Laravel CLI

  • MySQL/PostgreSQL

  • A web browser

Step 1: Install Laravel 12

First, create a new Laravel project:

composer create-project laravel/laravel example-app

Open .env file and update the DB settings:

DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=example-app-crud DB_USERNAME=root DB_PASSWORD=

Step 2: Install Laravel Breeze with Inertia.js + Vue

1. Install Breeze

composer require laravel/breeze --dev

2. Install Inertia Vue stack

php artisan breeze:install vue

3. Install frontend dependencies

npm install

4. Compile frontend assets

npm run dev

5. Migrate default tables

php artisan migrate

Step 3: Create the Posts Table and Model

1. Generate migration

php artisan make:migration create_posts_table

2. Define the schema

Edit the generated file in database/migrations:

Schema::create('posts', function (Blueprint $table) { $table->id(); $table->string('title'); $table->text('body'); $table->timestamps(); });

3. Run migration

php artisan migrate

4. Create Post model

php artisan make:model Post

5. Update model

// app/Models/Post.php protected $fillable = ['title', 'body'];

Step 4: Create Post Controller

php artisan make:controller PostController

Then update app/Http/Controllers/PostController.php:

namespace App\Http\Controllers; use App\Models\Post; use Illuminate\Http\Request; use Inertia\Inertia; class PostController extends Controller { public function index() { return Inertia::render('Post/Index', [ 'posts' => Post::all(), ]); } public function create() { return Inertia::render('Post/Create'); } public function store(Request $request) { Post::create($request->validate([ 'title' => 'required|string|max:255', 'body' => 'required|string', ])); return redirect()->route('posts.index'); } public function edit(Post $post) { return Inertia::render('Post/Edit', ['post' => $post]); } public function update(Request $request, Post $post) { $post->update($request->validate([ 'title' => 'required|string|max:255', 'body' => 'required|string', ])); return redirect()->route('posts.index'); } public function destroy(Post $post) { $post->delete(); return redirect()->back(); } }

Step 5: Define Routes

Open routes/web.php and add:

use App\Http\Controllers\PostController; Route::middleware(['auth', 'verified'])->group(function () { Route::resource('posts', PostController::class); });

Step 6: Create Vue Pages for CRUD

Create Folder:

mkdir resources/js/Pages/Post

🔹 Index.vue - List Posts

<script setup> import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue'; import { Head, Link, useForm } from '@inertiajs/vue3'; import { ref } from 'vue' defineProps({ posts: { type: Array, default: () => [], }, }); const form = useForm({}); const showDeleteModal = ref(false) const postIdToDelete = ref(null) const deletePost = (id) => { form.delete(`posts/${id}`); }; function openDeleteModal(id) { postIdToDelete.value = id showDeleteModal.value = true } function cancelDelete() { postIdToDelete.value = null showDeleteModal.value = false } function confirmDelete() { deletePost(postIdToDelete.value) showDeleteModal.value = false } </script> <template> <Head title="Manage Posts" /> <AuthenticatedLayout> <template #header> <h2 class="font-semibold text-xl text-gray-800 leading-tight">Manage Posts</h2> </template> <div class="py-12"> <div class="max-w-7xl mx-auto sm:px-6 lg:px-8"> <div class="bg-white overflow-hidden shadow-sm sm:rounded-lg"> <div class="p-6 text-gray-900"> <Link href="posts/create"> <button class="bg-blue-500 hover:bg-blue-700 text-white text-sm font-semibold py-1 px-3 rounded my-3 flex items-center gap-1"> ➕ Create </button> </Link> <table class="table-auto w-full"> <thead> <tr class="text-left bg-gray-200"> <th class="border px-4 py-2">ID</th> <th class="border px-4 py-2">Title</th> <th class="border px-4 py-2">Content</th> <th class="border px-4 py-2 text-center" width="180px">Action</th> </tr> </thead> <tbody> <tr v-for="(post, index) in posts" :key="post.id" class="text-left hover:bg-gray-100"> <td class="border px-4 py-2">{{ index + 1 }}</td> <td class="border px-4 py-2">{{ post.title }}</td> <td class="border px-4 py-2">{{ post.body }}</td> <td class="border px-4 py-2 text-center"> <Link :href="`posts/${post.id}/edit`"> <button class="bg-blue-500 hover:bg-blue-700 text-white text-sm font-semibold py-1 px-2 rounded inline-flex items-center gap-1"> ✏️ Edit </button> </Link> <button class="bg-red-500 hover:bg-red-700 text-white text-sm font-semibold py-1 px-2 rounded inline-flex items-center gap-1 ml-2" @click="openDeleteModal(post.id)"> 🗑️ Delete </button> </td> </tr> </tbody> </table> </div> </div> </div> </div> </AuthenticatedLayout> <!-- Delete Confirmation Modal --> <div v-if="showDeleteModal" class="fixed inset-0 flex items-center justify-center z-50 bg-black bg-opacity-50"> <div class="bg-white rounded-lg shadow-md w-full max-w-sm p-6"> <h2 class="text-lg font-semibold text-gray-800 mb-4">Confirm Deletion</h2> <p class="text-gray-600 mb-6">Are you sure you want to delete this post?</p> <div class="flex justify-end space-x-2"> <button @click="cancelDelete" class="bg-gray-300 hover:bg-gray-400 text-gray-800 font-semibold py-1 px-4 rounded"> Cancel </button> <button @click="confirmDelete" class="bg-red-500 hover:bg-red-700 text-white font-semibold py-1 px-4 rounded"> Yes, Delete </button> </div> </div> </div> </template>

🔹 Create.vue - Create Post

<script setup> import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue'; import { Head, useForm, Link } from "@inertiajs/vue3"; const form = useForm({ title: "", body: "", }); const submit = () => { form.post("/posts"); }; </script> <template> <Head title="Create Post" /> <AuthenticatedLayout> <template #header> <h2 class="font-semibold text-xl text-gray-800 leading-tight">Create Post</h2> </template> <div class="py-12"> <div class="max-w-3xl mx-auto sm:px-6 lg:px-8"> <div class="bg-white overflow-hidden shadow-sm sm:rounded-lg"> <div class="p-6 text-gray-900"> <!-- Back Button --> <Link href="/posts"> <button class="bg-gray-500 hover:bg-gray-700 text-white text-sm font-semibold py-1 px-3 rounded inline-flex items-center gap-1 mb-4"> 🔙 Back </button> </Link> <!-- Post Form --> <form @submit.prevent="submit"> <div class="mb-4"> <label for="title" class="block text-gray-700 text-sm font-bold mb-2">Title:</label> <input id="title" v-model="form.title" type="text" placeholder="Enter Title" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" /> <div v-if="form.errors.title" class="text-red-500 text-sm mt-1">{{ form.errors.title }}</div> </div> <div class="mb-4"> <label for="body" class="block text-gray-700 text-sm font-bold mb-2">Body:</label> <textarea id="body" v-model="form.body" placeholder="Enter Body" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" ></textarea> <div v-if="form.errors.body" class="text-red-500 text-sm mt-1">{{ form.errors.body }}</div> </div> <!-- Submit Button --> <button type="submit" class="bg-blue-500 hover:bg-blue-700 text-white text-sm font-semibold py-2 px-4 rounded inline-flex items-center gap-1" :disabled="form.processing" > ✅ Submit </button> </form> </div> </div> </div> </div> </AuthenticatedLayout> </template>

🔹 Edit.vue - Edit Post

<script setup> import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue'; import { Head, useForm, Link } from "@inertiajs/vue3"; // Props const props = defineProps({ post: { type: Object, required: true, }, }); // Form with initial data from props const form = useForm({ title: props.post.title, body: props.post.body, }); // Submit update request const submit = () => { form.put(`/posts/${props.post.id}`); }; </script> <template> <Head title="Edit Post" /> <AuthenticatedLayout> <template #header> <h2 class="font-semibold text-xl text-gray-800 leading-tight">Edit Post</h2> </template> <div class="py-12"> <div class="max-w-3xl mx-auto sm:px-6 lg:px-8"> <div class="bg-white overflow-hidden shadow-sm sm:rounded-lg"> <div class="p-6 text-gray-900"> <!-- Back Button --> <Link href="/posts"> <button class="bg-gray-500 hover:bg-gray-700 text-white text-sm font-semibold py-1 px-3 rounded inline-flex items-center gap-1 mb-4"> 🔙 Back </button> </Link> <!-- Edit Form --> <form @submit.prevent="submit"> <div class="mb-4"> <label for="title" class="block text-gray-700 text-sm font-bold mb-2">Title:</label> <input id="title" type="text" v-model="form.title" placeholder="Enter Title" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" /> <div v-if="form.errors.title" class="text-red-500 text-sm mt-1">{{ form.errors.title }}</div> </div> <div class="mb-4"> <label for="body" class="block text-gray-700 text-sm font-bold mb-2">Body:</label> <textarea id="body" v-model="form.body" placeholder="Enter Body" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" ></textarea> <div v-if="form.errors.body" class="text-red-500 text-sm mt-1">{{ form.errors.body }}</div> </div> <!-- Submit Button --> <button type="submit" class="bg-blue-500 hover:bg-blue-700 text-white text-sm font-semibold py-2 px-4 rounded inline-flex items-center gap-1" :disabled="form.processing" > 💾 Update </button> </form> </div> </div> </div> </div> </AuthenticatedLayout> </template>

Step 7: Add Posts to Navigation Menu

Open resources/js/Layouts/AuthenticatedLayout.vue, and inside your navigation area:

<NavLink :href="route('posts.index')" :active="route().current('posts.index')"> Posts </NavLink>

Step 8: Run the App

Start Laravel and the Vite dev server:

php artisan serve npm run dev

Visit: http://localhost:8000/posts

Laravel Backend (Server-Side)

app/ │ ├── Http/ │ ├── Controllers/ │ │ ├── PostController.php │ │ └── Auth/ │ └── Middleware/ │ ├── Models/ │ └── Post.php │ ├── Policies/ │ └── Providers/

Routes

routes/ ├── web.php # Inertia (Vue) frontend routes ├── api.php # Optional (if using separate API)

Vue + Inertia Frontend (Client-Side)

resources/ └── js/ ├── App.vue # Main Vue root component ├── bootstrap.js # Inertia and Vue setup ├── Pages/ # Inertia pages (one file = one route) │ ├── Dashboard.vue │ └── Posts/ │ ├── Index.vue # List of posts │ ├── Create.vue # Create post form │ └── Edit.vue # Edit post form │ ├── Components/ # Reusable components │ ├── NavLink.vue │ ├── Pagination.vue │ └── FormInput.vue │ └── Layouts/ ├── AuthenticatedLayout.vue └── GuestLayout.vue

Views

resources/views/ └── app.blade.php # Blade template for Inertia entrypoint

Public

public/ └── images/ # Optional for uploaded images

Database

database/ ├── factories/ │ └── PostFactory.php ├── migrations/ │ └── 2024_01_01_create_posts_table.php └── seeders/ └── PostSeeder.php

Summary of Key Files

FilePurpose
PostController.phpHandles CRUD logic
resources/js/Pages/Posts/*.vueVue pages for the Post entity
AuthenticatedLayout.vueShared layout for authenticated users
NavLink.vueNavigation component with active route
web.phpLaravel routes pointing to Inertia pages
app.blade.phpBlade view mounting Vue SPA

Optional: Add Styling

Breeze uses Tailwind CSS by default. You can customize your components using Tailwind classes, or enhance them with tools like:

Conclusion

You now have a fully working CRUD application using:

  • ✅ Laravel 12

  • ✅ Inertia.js

  • ✅ Vue 3

  • ✅ Tailwind CSS

This modern stack is great for building reactive, SEO-friendly apps with server-side simplicity and client-side interactivity.

Souy Soeng

Souy Soeng

Hi there 👋, I’m Soeng Souy (StarCode Kh)
-------------------------------------------
🌱 I’m currently creating a sample Laravel and React Vue Livewire
👯 I’m looking to collaborate on open-source PHP & JavaScript projects
💬 Ask me about Laravel, MySQL, or Flutter
⚡ Fun fact: I love turning ☕️ into code!

Post a Comment

CAN FEEDBACK
close