In modern web development, combining Laravel, Vue.js, InertiaJS, and Tailwind CSS provides a seamless way to build fast, responsive, and dynamic single-page applications (SPAs). Laravel handles backend logic, Vue manages frontend reactivity, Inertia connects both sides smoothly, and Tailwind helps us style everything beautifully.
In this blog post, we’ll walk through building a full CRUD (Create, Read, Update, Delete) application in Laravel 12 with Vue 3, InertiaJS, and Tailwind CSS.
✨ What We’ll Build
A simple Posts Management System where users can:
- View all posts
- Add a new post
- Edit a post
- Delete a post
🔧 Tools & Versions
- Laravel: 12.x
- Vue.js: 3.x
- Inertia.js: Latest
- Tailwind CSS: 3.x
- Vite: For asset bundling
🚀 Step 1: Install Laravel 12
Install a new Laravel project via Composer:
composer create-project laravel/laravel laravel-vue-crud
cd laravel-vue-crud
⚙️ Step 2: Install Inertia.js and Vue
Install Inertia Laravel adapter:
composer require inertiajs/inertia-laravel
php artisan inertia:middleware
Once the middleware has been published, append the HandleInertiaRequests middleware to the web middleware group in your application’s bootstrap/app.php file.
use App\Http\Middleware\HandleInertiaRequests;
->withMiddleware(function (Middleware $middleware) {
$middleware->web(append: [
HandleInertiaRequests::class,
]);
})
Root template
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
@vite('resources/js/app.js')
@inertiaHead
</head>
<body>
@inertia
</body>
</html>
Install Vue and Inertia.js (client-side):
npm install @inertiajs/vue3
🎨 Step 3: Install and Configure Tailwind CSS
Install Tailwind CSS:
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
Update tailwind.config.js:
content: [
"./resources/**/*.blade.php",
"./resources/**/*.js",
"./resources/**/*.vue",
],
Add Tailwind directives in resources/css/app.css:
@tailwind base;
@tailwind components;
@tailwind utilities;
🧱 Step 4: Setup Vite + Vue + Inertia
Update resources/js/app.js:
import './bootstrap';
import { createApp, h } from 'vue'
import { createInertiaApp, Head, Link } from '@inertiajs/vue3'
createInertiaApp({
title:(title) => ` Project Name ${title}`,
resolve: name => {
const pages = import.meta.glob('./Pages/**/*.vue', { eager: true })
let page = pages[`./Pages/${name}.vue`]
page.default.layout = page.default.layout || Layout;
return page;
},
setup({ el, App, props, plugin }) {
createApp({ render: () => h(App, props) })
.use(plugin)
.component('Head',Head)
.component('Link',Link)
.mount(el)
},progress: {
color: 'red',
includeCSS: true,
showSpinner: false,
},
})
Update vite.config.js:
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': '/resources/js',
},
},
})
📁 Step 5: Create Post Model, Migration & Controller
Generate the model and migration:
php artisan make:model Post -m
Update the migration file:
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('content');
$table->timestamps();
});
Run migration:
php artisan migrate
Generate controller:
php artisan make:controller PostController --resource
🛠️ Step 6: Define Web Routes
In routes/web.php:
use App\Http\Controllers\PostController;
Route::get('/', fn () => redirect('/posts'));
Route::resource('posts', PostController::class);
📦 Step 7: Implement PostController with Inertia
Update PostController.php:
use App\Models\Post;
use Illuminate\Http\Request;
use Inertia\Inertia;
class PostController extends Controller
{
public function index()
{
return Inertia::render('Posts/Index', [
'posts' => Post::latest()->get(),
]);
}
public function create()
{
return Inertia::render('Posts/Create');
}
public function store(Request $request)
{
$request->validate([
'title' => 'required',
'content' => 'required',
]);
Post::create($request->only('title', 'content'));
return redirect()->route('posts.index')->with('success', 'Post created!');
}
public function edit(Post $post)
{
return Inertia::render('Posts/Edit', ['post' => $post]);
}
public function update(Request $request, Post $post)
{
$request->validate([
'title' => 'required',
'content' => 'required',
]);
$post->update($request->only('title', 'content'));
return redirect()->route('posts.index')->with('success', 'Post updated!');
}
public function destroy(Post $post)
{
$post->delete();
return redirect()->route('posts.index')->with('success', 'Post deleted!');
}
}
🧩 Step 8: Create Vue Pages
Create directory:
mkdir -p resources/js/Pages/Posts
📄 Index.vue
<template>
<div class="p-6">
<div class="flex justify-between items-center mb-4">
<h1 class="text-2xl font-bold">All Posts</h1>
<Link href="/posts/create" class="bg-blue-500 text-white px-4 py-2 rounded">Create</Link>
</div>
<div v-if="posts.length">
<div v-for="post in posts" :key="post.id" class="border p-4 mb-2">
<h2 class="font-bold text-lg">{{ post.title }}</h2>
<p>{{ post.content }}</p>
<div class="mt-2 space-x-2">
<Link :href="`/posts/${post.id}/edit`" class="text-blue-500">Edit</Link>
<button @click="deletePost(post.id)" class="text-red-500">Delete</button>
</div>
</div>
</div>
<div v-else>No posts found.</div>
</div>
</template>
<script setup>
import { router } from '@inertiajs/inertia'
import { Link } from '@inertiajs/inertia-vue3'
defineProps(['posts'])
function deletePost(id) {
if (confirm('Are you sure?')) {
router.delete(`/posts/${id}`)
}
}
</script>
📝 Create.vue and Edit.vue
Create a shared form component: PostForm.vue
<template>
<form @submit.prevent="submit" class="space-y-4">
<input v-model="form.title" type="text" placeholder="Title" class="w-full border p-2 rounded" />
<textarea v-model="form.content" placeholder="Content" class="w-full border p-2 rounded"></textarea>
<button type="submit" class="bg-green-500 text-white px-4 py-2 rounded">
{{ isEdit ? 'Update' : 'Create' }} Post
</button>
</form>
</template>
<script setup>
import { useForm } from '@inertiajs/inertia-vue3'
import { computed } from 'vue'
const props = defineProps({ post: Object })
const isEdit = computed(() => !!props.post)
const form = useForm({
title: props.post?.title || '',
content: props.post?.content || '',
})
function submit() {
if (isEdit.value) {
form.put(`/posts/${props.post.id}`)
} else {
form.post('/posts')
}
}
</script>
Use this in Create.vue:
<template>
<div class="p-6">
<h1 class="text-xl font-bold mb-4">Create Post</h1>
<PostForm />
</div>
</template>
<script setup>
import PostForm from './PostForm.vue'
</script>
And in Edit.vue:
<template>
<div class="p-6">
<h1 class="text-xl font-bold mb-4">Edit Post</h1>
<PostForm :post="post" />
</div>
</template>
<script setup>
import PostForm from './PostForm.vue'
defineProps({ post: Object })
</script>
🧠 Step 9: Handle Flash Messages
In app/Http/Middleware/HandleInertiaRequests.php, add shared flash:
'flash' => fn () => [
'success' => session('success'),
],
Display flash in layout:
<template>
<div>
<div v-if="$page.props.flash.success" class="bg-green-100 text-green-800 p-3 rounded mb-4">
{{ $page.props.flash.success }}
</div>
<slot />
</div>
</template>
<script setup>
import { usePage } from '@inertiajs/inertia-vue3'
</script>
🧩 Final Steps
php artisan serve
npm run dev
✅ Conclusion
You’ve now built a fully functional CRUD application using Laravel 12, Vue 3, InertiaJS, and Tailwind CSS. This setup gives you the power of Laravel on the backend with the flexibility and responsiveness of Vue on the frontend — all without building a separate API.


Leave a Reply