Kritim Yantra
Feb 27, 2025
This project demonstrates how to build a simple CRUD (Create, Read, Update, Delete) application using Laravel 12 paired with Vue.js 3, InertiaJS, and Tailwind CSS. You'll learn how to set up your Laravel project with Vue starter kits, create the necessary models, migrations, and controllers, and then build a clean, responsive frontend using Vue components styled with Tailwind CSS. The result is a modern, user-friendly interface that allows you to manage posts seamlessly, making it an ideal starting point for full-stack web development.
If you have the Laravel installer installed, create a new project by running:
laravel new laravelvuecrud
This command will create a fresh Laravel 12 application in a folder named laravelvuecrud.
To leverage a modern Vue 3 frontend with InertiaJS, choose vue when laravel asks to choose starter kit:
We’ll manage blog posts, so let’s create the Post model, its migration, and a resource controller.
Create the Post Model with a Migration:
php artisan make:model Post -m
Create a Resource Controller for Posts:
php artisan make:controller PostController --resource
Now, open the generated migration file (located in database/migrations
) and update it to include the necessary fields. For example:
// database/migrations/xxxx_xx_xx_create_posts_table.php
public function up()
{
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('content');
$table->timestamps();
});
}
Then, run the migration to create the table:
php artisan migrate
Your Vue files for handling CRUD operations will typically reside in resources/js/Pages/Posts
. Below are the key files and their code examples.
resources/js/Pages/Posts/Index.vue
This page displays all posts in a table along with “Edit” and “Delete” actions.
<!-- File: resources/js/Pages/Posts/Index.vue -->
<template>
<AppLayout :breadcrumbs="breadcrumbs">
<Head title="Posts" />
<div class="container mx-auto p-4">
<div class="flex justify-between items-center mb-4">
<h1 class="text-2xl font-bold">Blog Posts</h1>
<Link href="/posts/create" class="bg-gray-500 text-white px-4 py-1 rounded hover:bg-gray-600">
Create
</Link>
</div>
<div class="overflow-x-auto">
<table class="min-w-full bg-white shadow rounded-lg">
<thead>
<tr class="bg-gray-200">
<th class="py-2 px-4 text-left border-b">Name</th>
<th class="py-2 px-4 text-left border-b">Content</th>
<th class="py-2 px-4 text-left border-b">Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="post in posts" :key="post.id" class="hover:bg-gray-50">
<td class="py-2 px-4 border-b">{{ post.title }}</td>
<td class="py-2 px-4 border-b">{{ post.content }}</td>
<td class="py-2 px-4 border-b">
<Link :href="`/posts/${post.id}/edit`" class="bg-green-500 text-white px-2 py-1 rounded hover:bg-green-600 mr-2">
Edit
</Link>
<Link
:href="route('posts.destroy', post.id)"
method="delete"
class="bg-red-500 text-white px-2 py-1 rounded hover:bg-red-600"
@click.prevent="confirmDelete($event)"
>
Delete
</Link>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</AppLayout>
</template>
<script setup>
import { Head, Link } from '@inertiajs/inertia-vue3'
import AppLayout from '@/Layouts/AppLayout.vue'
// Props
const props = defineProps({
posts: Array,
})
// Breadcrumbs
const breadcrumbs = [
{ title: 'Posts', href: '/posts' },
]
// Confirm delete handler
function confirmDelete(e) {
if (!confirm('Are you sure?')) {
e.preventDefault()
}
}
</script>
resources/js/Pages/Posts/Create.vue
This page renders a form for creating a new post using InertiaJS’s form handling.
<!-- File: resources/js/Pages/Posts/Create.vue -->
<template>
<AppLayout :breadcrumbs="breadcrumbs">
<Head title="Create Post" />
<div class="container mx-auto p-4">
<div class="flex justify-between items-center mb-4">
<h1 class="text-2xl font-bold">Blog Posts</h1>
</div>
<div class="bg-white shadow rounded-lg p-4 mb-6">
<h2 class="text-xl font-bold mb-4">Add New Post</h2>
<form @submit.prevent="submit">
<div class="mb-4">
<label for="title" class="block text-gray-700 font-medium mb-1">Title</label>
<input
type="text"
id="title"
v-model="form.title"
placeholder="Post Title"
class="w-full border border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring focus:ring-blue-200"
/>
<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="content" class="block text-gray-700 font-medium mb-1">Content</label>
<textarea
id="content"
v-model="form.content"
placeholder="Post Content"
rows="4"
class="w-full border border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring focus:ring-blue-200"
></textarea>
<div v-if="form.errors.content" class="text-red-500 text-sm mt-1">
{{ form.errors.content }}
</div>
</div>
<button type="submit" :disabled="form.processing" class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">
{{ form.processing ? 'Saving...' : 'Save' }}
</button>
</form>
</div>
</div>
</AppLayout>
</template>
<script setup>
import { Head, Link, useForm } from '@inertiajs/inertia-vue3'
import AppLayout from '@/Layouts/AppLayout.vue'
// Breadcrumbs
const breadcrumbs = [
{ title: 'Posts', href: '/posts' },
]
// Setup form
const form = useForm({
title: '',
content: '',
})
// Submit handler
function submit() {
form.post(route('posts.store'), {
preserveScroll: true,
onSuccess: () => form.reset(),
})
}
</script>
resources/js/Pages/Posts/Edit.vue
This page lets you edit an existing post. It pre-fills the form with the post data passed as a prop.
<!-- File: resources/js/Pages/Posts/Edit.vue -->
<template>
<AppLayout :breadcrumbs="breadcrumbs">
<Head title="Edit Post" />
<div class="container mx-auto p-4">
<div class="flex justify-between items-center mb-4">
<h1 class="text-2xl font-bold">Blog Posts</h1>
</div>
<div class="bg-white shadow rounded-lg p-4 mb-6">
<h2 class="text-xl font-bold mb-4">Edit Post</h2>
<form @submit.prevent="submit">
<div class="mb-4">
<label for="title" class="block text-gray-700 font-medium mb-1">Title</label>
<input
type="text"
id="title"
v-model="form.title"
placeholder="Post Title"
class="w-full border border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring focus:ring-blue-200"
/>
<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="content" class="block text-gray-700 font-medium mb-1">Content</label>
<textarea
id="content"
v-model="form.content"
placeholder="Post Content"
rows="4"
class="w-full border border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring focus:ring-blue-200"
></textarea>
<div v-if="form.errors.content" class="text-red-500 text-sm mt-1">
{{ form.errors.content }}
</div>
</div>
<button type="submit" :disabled="form.processing" class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">
{{ form.processing ? 'Updating...' : 'Update' }}
</button>
</form>
</div>
</div>
</AppLayout>
</template>
<script setup>
import { Head, useForm } from '@inertiajs/inertia-vue3'
import AppLayout from '@/Layouts/AppLayout.vue'
// Receive the post prop
const props = defineProps({
post: Object,
})
// Breadcrumbs
const breadcrumbs = [
{ title: 'Posts', href: '/posts' },
]
// Setup form with post data
const form = useForm({
title: props.post.title,
content: props.post.content,
})
// Submit handler
function submit() {
form.put(route('posts.update', props.post.id), {
preserveScroll: true,
onSuccess: () => form.reset(),
})
}
</script>
resources/js/Pages/Dashboard.vue
A simple dashboard page with a button linking to the posts page:
<!-- File: resources/js/Pages/Dashboard.vue -->
<template>
<div class="p-4">
<button class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">
<Link href="/posts">Posts</Link>
</button>
</div>
</template>
<script setup>
import { Link } from '@inertiajs/inertia-vue3'
</script>
Finally, update the PostController to handle CRUD operations. For example:
<?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('Posts/Index', [
'posts' => Post::all(),
]);
}
public function create()
{
return Inertia::render('Posts/Create');
}
public function store(Request $request)
{
Post::create([
'title' => $request->title,
'content' => $request->content,
]);
return redirect()->route('posts.index');
}
public function edit(Post $post)
{
return Inertia::render('Posts/Edit', [
'post' => $post,
]);
}
public function update(Request $request, Post $post)
{
$post->update([
'title' => $request->title,
'content' => $request->content,
]);
return redirect()->route('posts.index');
}
public function destroy(Post $post)
{
$post->delete();
return redirect()->route('posts.index');
}
}
routes/web.php
:
use App\Http\Controllers\PostController;
Route::resource('posts', PostController::class);
php artisan serve
This guide walked you through every step—from creating a Laravel project with Vue starter kits to setting up your model, migration, and controller, and finally building Vue components for CRUD operations. With Laravel 12, Vue 3, InertiaJS, and Tailwind CSS, you can create a modern, responsive web application that’s both powerful and user friendly.
Happy coding, and enjoy building your Laravel Vue CRUD application!
No comments yet. Be the first to comment!
Please log in to post a comment:
Sign in with Google