In this tutorial, we will create a CRUD app using Laravel 10 and Inertia.js with Vue 3. Laravel 10 introduces some changes in Inertia.js with Vue 3, so if you’re familiar with Laravel 9, be sure to read the blog below for insights on these updates.
Step 1: Install Laravel & Connect Database
To install a fresh new Laravel application, navigate to your terminal, type the command, and create a new Laravel app.
composer create-project laravel/laravel inertia-vue
Now, you need to connect the Laravel app to the database. Open the .env configuration file and add the database credentials as suggested below.
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=database_name
DB_USERNAME=database_user_name
DB_PASSWORD=database_password
Step 2: Install Breeze & Setup Inertia Js Vue 3
Install laravel breeze via composer.
composer require laravel/breeze --dev
Next, run below command.
php artisan breeze:install
Now select vue.
Which Breeze stack would you like to install?
Blade ............................................................... blade
React with Inertia .................................................. react
Vue with Inertia .................................................... vue
API only ............................................................ api
❯ vue
Step 3: Create Blog Modal Migration and Controller Route
Run below command to create blog modal, migration and controller.
php artisan make:model Blog -mcr
create_blogs_table.php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('blogs', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('content');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('blogs');
}
};
App/Models/Blog.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Blog extends Model
{
use HasFactory;
protected $fillable = [
'title',
'content'
];
}
Create blogs routes.
<?php
use App\Http\Controllers\BlogController;
use App\Http\Controllers\ProfileController;
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;
/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/
Route::get('/', function () {
return Inertia::render('Welcome', [
'canLogin' => Route::has('login'),
'canRegister' => Route::has('register'),
'laravelVersion' => Application::VERSION,
'phpVersion' => PHP_VERSION,
]);
});
Route::get('/dashboard', function () {
return Inertia::render('Dashboard');
})->middleware(['auth', 'verified'])->name('dashboard');
Route::middleware('auth')->group(function () {
Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
});
Route::resource('blogs',BlogController::class);
require __DIR__.'/auth.php';
app/Http/Controllers/BlogController.php
<?php
namespace App\Http\Controllers;
use App\Models\Blog;
use Illuminate\Http\Request;
use Inertia\Inertia;
class BlogController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
{
$blogs = Blog::all();
return Inertia::render(
'Blogs/Index',
[
'blogs' => $blogs
]
);
}
/**
* Show the form for creating a new resource.
*/
public function create()
{
return Inertia::render(
'Blogs/Create'
);
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
$request->validate([
'title' => 'required|string|max:255',
'content' => 'required'
]);
Blog::create([
'title' => $request->title,
'content' => $request->content
]);
sleep(1);
return redirect()->route('blogs.index')->with('message', 'Blog Created Successfully');
}
/**
* Display the specified resource.
*/
public function show(Blog $blog)
{
//
}
/**
* Show the form for editing the specified resource.
*/
public function edit(Blog $blog)
{
return Inertia::render(
'Blogs/Edit',
[
'blog' => $blog
]
);
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, Blog $blog)
{
$request->validate([
'title' => 'required|string|max:255',
'content' => 'required'
]);
$blog->title = $request->title;
$blog->content = $request->content;
$blog->save();
sleep(1);
return redirect()->route('blogs.index')->with('message', 'Blog Updated Successfully');
}
/**
* Remove the specified resource from storage.
*/
public function destroy(Blog $blog)
{
$blog->delete();
sleep(1);
return redirect()->route('blogs.index')->with('message', 'Blog Delete Successfully');
}
}
Step 4: Create Blog View File
Next, you need to create Index.vue
, Create.vue
, and Edit.vue
files and implement Vue 3 to perform CRUD operations.
Blogs/Create.vue
<script setup>
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout.vue";
import TextInput from "@/Components/TextInput.vue";
import PrimaryButton from "@/Components/PrimaryButton.vue";
import InputLabel from "@/Components/InputLabel.vue";
import InputError from "@/Components/InputError.vue";
import { Head, useForm } from "@inertiajs/vue3";
const props = defineProps({
blogs: {
type: Object,
default: () => ({}),
},
});
const form = useForm({
title: "",
content: "",
});
const submit = () => {
form.post(route("blogs.store"));
};
</script>
<template>
<Head title="Blog Create" />
<AuthenticatedLayout>
<template #header>
<h2 class="text-xl font-semibold leading-tight text-gray-800">
Blog Create
</h2>
</template>
<div class="py-12">
<div class="mx-auto max-w-7xl">
<div class="overflow-hidden bg-white shadow-sm sm:rounded-lg">
<div class="p-6 bg-white border-b border-gray-200">
<form @submit.prevent="submit">
<div>
<InputLabel for="title" value="Title" />
<TextInput
id="title"
type="text"
class="mt-1 block w-full"
v-model="form.title"
required
autofocus
autocomplete="username"
/>
<InputError
class="mt-2"
:message="form.errors.title"
/>
</div>
<div class="my-6">
<label
for="content"
class="block mb-2 text-sm font-medium text-gray-900"
>Content</label
>
<textarea
type="text"
v-model="form.content"
name="content"
id=""
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
></textarea>
<div
v-if="form.errors.content"
class="text-sm text-red-600"
>
{{ form.errors.content }}
</div>
</div>
<PrimaryButton
type="submit"
:class="{ 'opacity-25': form.processing }"
:disabled="form.processing"
>
Submit
</PrimaryButton>
</form>
</div>
</div>
</div>
</div>
</AuthenticatedLayout>
</template>
Blogs/Index.vue
<script setup>
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout.vue";
import PrimaryButton from "@/Components/PrimaryButton.vue";
import { Head, Link, useForm } from "@inertiajs/vue3";
const props = defineProps({
blogs: {
type: Object,
default: () => ({}),
},
});
const form = useForm({});
function destroy(id) {
if (confirm("Are you sure you want to Delete")) {
form.delete(route("blogs.destroy", id));
}
}
</script>
<template>
<Head title="Blogs" />
<AuthenticatedLayout>
<template #header>
<h2 class="text-xl font-semibold leading-tight text-gray-800">
Blogs Index
</h2>
</template>
<div class="py-12">
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
<div class="overflow-hidden bg-white shadow-sm sm:rounded-lg">
<div class="p-6 bg-white border-b border-gray-200">
<div class="mb-2">
<Link :href="route('blogs.create')">
<PrimaryButton>Add Blog</PrimaryButton>
</Link>
</div>
<div class="relative overflow-x-auto shadow-md sm:rounded-lg">
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400">
<thead
class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
<tr>
<th scope="col" class="px-6 py-3">#</th>
<th scope="col" class="px-6 py-3">
Title
</th>
<th scope="col" class="px-6 py-3">
Edit
</th>
<th scope="col" class="px-6 py-3">
Delete
</th>
</tr>
</thead>
<tbody>
<tr v-for="blog in blogs" :key="blog.id"
class="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
<th scope="row"
class="px-6 py-4 font-medium text-gray-900 dark:text-white whitespace-nowrap">
{{ blog.id }}
</th>
<th scope="row"
class="px-6 py-4 font-medium text-gray-900 dark:text-white whitespace-nowrap">
{{ blog.title }}
</th>
<td class="px-6 py-4">
<Link :href="route('blogs.edit', blog.id)
" class="px-4 py-2 text-white bg-blue-600 rounded-lg">Edit</Link>
</td>
<td class="px-6 py-4">
<PrimaryButton class="bg-red-700" @click="destroy(blog.id)">
Delete
</PrimaryButton>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</AuthenticatedLayout>
</template>
Blogs/Edit.vue
<script setup>
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout.vue";
import TextInput from '@/Components/TextInput.vue';
import InputLabel from '@/Components/InputLabel.vue';
import InputError from '@/Components/InputError.vue';
import PrimaryButton from "@/Components/PrimaryButton.vue";
import { Head, useForm } from "@inertiajs/vue3";
const props = defineProps({
blog: {
type: Object,
default: () => ({}),
},
});
const form = useForm({
id: props.blog.id,
title: props.blog.title,
content: props.blog.content,
});
const submit = () => {
form.put(route("blogs.update", props.blog.id));
};
</script>
<template>
<Head title="Blog Edit" />
<AuthenticatedLayout>
<template #header>
<h2 class="text-xl font-semibold leading-tight text-gray-800">
Blog Edit
</h2>
</template>
<div class="py-12">
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
<div class="overflow-hidden bg-white shadow-sm sm:rounded-lg">
<div class="p-6 bg-white border-b border-gray-200">
<form @submit.prevent="submit">
<div>
<InputLabel for="title" value="Title" />
<TextInput id="title" type="text" class="mt-1 block w-full" v-model="form.title" required
autofocus autocomplete="username" />
<InputError class="mt-2" :message="form.errors.title" />
</div>
<div class="my-6">
<label for="slug"
class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300">Content</label>
<textarea type="text" v-model="form.content" name="content" id=""
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"></textarea>
<div v-if="form.errors.content" class="text-sm text-red-600">
{{ form.errors.content }}
</div>
</div>
<PrimaryButton type="submit" :class="{ 'opacity-25': form.processing }"
:disabled="form.processing">
Submit
</PrimaryButton>
</form>
</div>
</div>
</div>
</div>
</AuthenticatedLayout>
</template>
Step 5: Run Application Server
run the serve
php artisan serve
run vite server
npm run dev
# or
npm run build