In this tutorial we’ll create Laravel 10 CRUD app using VILT (Vue Inertia Laravel Tailwind) stack. Laravel 10.x requires a minimum PHP version of 8.1. This tutorial assumes that you are using Linux, macOS or WSL on Windows and Node.js, Composer and PHP 8.1 along with required modules i.e bcmatch, sqlite, mbstring, xml, zip, gd, mcrypt, curl are properly installed. Also make sure that SQLite is installed as we’ll be using SQLite to keep things simple and quick; you may use MySQL or PostgreSQL as you like, however the same will require the corresponding PHP module installed too.
Using Composer, enter following command in terminal to install Laravel 10:
composer create-project laravel/laravel app 10.*
Go inside app folder:
cd app
If you want to open the project in VSCode, enter following command:
code .
Now, add Breeze package by executing following command:
composer require laravel/breeze:* --dev
Then install frontend scaffolding based on VILT stack:
php artisan breeze:install vue
The above command will install the required npm modules and then build the UI assets.
Next, setup database and Laravel environment file. Here, we’re using SQLite database for simplicity purposes. Being inside parent folder (app), execute following command to create new SQLite database file:
touch database/database.sqlite
Then find .env config file in the parent folder, open it and change DB_CONNECTION line as follows:
DB_CONNECTION = sqlite
Remove DB_HOST, DB_PORT, DB_DATABASE, DB_USERNAME, DB_PASSWORD from .env since we’re using SQLite database.
Initial setup of Laravel app is done! we can now start building the CRUD. First enter the following command to create Post model along with the migration and controller:
php artisan make:model Post -mrc
Open database/migrations/(date_stamp)_create_posts_table.php and then insert the table fields as follows:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('body');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('posts');
}
};
Enter following command to populate the database with tables:
php artisan migrate
Next, in order to enable mass-assignment for relevant data fields, open app/Models/Post.php model file and insert protected fillable array as follows:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
use HasFactory;
protected $fillable = [
'title', 'body'
];
}
Then, open app/Http/Controllers/PostController.php and amend it as follows in order to define basic CRUD (Create, Read, Update, Delete) functions:
<?php
namespace App\Http\Controllers;
use App\Models\Post;
use Illuminate\Http\Request;
use Illuminate\Http\RedirectResponse;
use Inertia\Inertia;
use Inertia\Response;
class PostController extends Controller
{
public function index(): Response
{
$data = Post::all();
return Inertia::render('Posts/Index', ['data' => $data]);
}
public function create(): Response
{
return Inertia::render('Posts/Create');
}
public function store(Request $request): RedirectResponse
{
$validated = $request->validate([
'title' => ['required', 'max:50'],
'body' => ['required'],
]);
$item = Post::create($validated);
return redirect(route('posts.index'))->with('success', 'Post created.');
}
public function show(Post $post): Response
{
//
}
public function edit(Post $post): Response
{
return Inertia::render('Posts/Edit', [
'post' => [
'id' => $post->id,
'title' => $post->title,
'body' => $post->body,
],
]);
}
public function update(Request $request, Post $post): RedirectResponse
{
$validated = $request->validate([
'title' => ['required', 'max:50'],
'body' => ['required'],
]);
$post->update($validated);
return redirect(route('posts.index'))->with('success', 'Post updated.');
}
public function destroy(Post $post): RedirectResponse
{
$post->delete();
return redirect(route('posts.index'))->with('success', 'Post deleted.');
}
}
In order to define resource route, open routes/web.php and put the following line at the upper side of it:
use App\Http\Controllers\PostController;
And then, insert following resource route down somewhere inside this routes/web.php:
Route::resource('posts', PostController::class)->middleware(['auth', 'verified']);
We’re done with the Model and Controller parts of the app. Now let’s create Views using Vue.
First of all, for the purpose of displaying flash messages in views, let’s change share() function of app/Http/Middleware/HandleInertiaRequests.php as follows:
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Request;
use Inertia\Middleware;
use Tightenco\Ziggy\Ziggy;
class HandleInertiaRequests extends Middleware
{
protected $rootView = 'app';
public function version(Request $request): string|null
{
return parent::version($request);
}
public function share(Request $request): array
{
return array_merge(parent::share($request), [
'auth' => [
'user' => $request->user(),
],
'ziggy' => function () use ($request) {
return array_merge((new Ziggy)->toArray(), [
'location' => $request->url(),
]);
},
'flash' => function () use ($request) {
return [
'success' => $request->session()->get('success'),
'error' => $request->session()->get('error'),
];
},
]);
}
}
Now, create Posts folder inside resources/js/Pages. Being inside parent folder (app), you may execute following command to create it:
mkdir resources/js/Pages/Posts
Now create following three files inside resources/js/Pages/Posts:
- Index.vue
- Create.vue
- Edit.vue
You can execute following command inside parent folder to create these files in one go:
touch resources/js/Pages/Posts/{Index,Create,Edit}.vue
Go inside resources/js/Pages/Posts folder and insert following code in Index.vue:
<script setup>
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue'
import PrimaryButton from '@/Components/PrimaryButton.vue'
import DangerButton from '@/Components/DangerButton.vue'
import { router, Head, Link } from '@inertiajs/vue3'
const props = defineProps({
data: Object,
})
const edit = (id) => {
router.visit(route('posts.edit', id))
}
const destroy = (id) => {
router.visit(route('posts.destroy', id), {method: 'delete'})
}
</script>
<template>
<Head title="Posts" />
<AuthenticatedLayout>
<h1 class="pl-6 py-2 text-3xl font-bold bg-gray-500 text-white">Posts</h1>
<div v-if="$page.props.flash.success" class="bg-gray-300 font-bold text-blue-900 p-1">
{{ $page.props.flash.success }}
</div>
<div class="m-4">
<Link :href="route('posts.create')" class="inline-flex items-center px-4 py-2 bg-green-800 border border-transparent rounded-lg font-semibold text-xs text-white uppercase tracking-widest hover:bg-green-700 active:bg-green-900 focus:outline-none focus:border-green-900 focus:ring ring-green-300 disabled:opacity-25 transition ease-in-out duration-150">
Create Post
</Link>
</div>
<div class="bg-white rounded-md shadow overflow-x-auto">
<table class="w-full whitespace-nowrap">
<thead>
<tr class="bg-gray-500 text-white font-extrabold">
<th class="px-4 py-1 border">Title</th>
<th class="px-4 py-1 border">Body</th>
<th class="px-4 py-1 border">Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="item in data" :key="item.id">
<td class="px-4 py-1 border">{{item.title}}</td>
<td class="px-4 py-1 border">{{item.body}}</td>
<td class="px-4 py-1 border">
<PrimaryButton @click="edit(item.id)" class="mr-4">
Edit
</PrimaryButton>
<DangerButton @click="destroy(item.id)">
Delete
</DangerButton>
</td>
</tr>
</tbody>
</table>
</div>
</AuthenticatedLayout>
</template>
Then, insert following code into Create.vue:
<script setup>
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue'
import InputError from '@/Components/InputError.vue'
import InputLabel from '@/Components/InputLabel.vue'
import TextInput from '@/Components/TextInput.vue'
import PrimaryButton from '@/Components/PrimaryButton.vue'
import { Head, useForm } from '@inertiajs/vue3'
const form = useForm({
title: null,
body: null,
})
</script>
<template>
<Head title="Posts" />
<AuthenticatedLayout>
<h1 class="pl-6 py-2 text-3xl font-bold bg-gray-500 text-white">Posts</h1>
<div class="min-h-screen flex flex-col sm:justify-start items-center pt-6 sm:pt-0 bg-gray-100">
<div class="w-full sm:max-w-md mt-6 px-6 py-4 bg-white shadow-md overflow-hidden sm:rounded-lg">
<form @submit.prevent="form.post(route('posts.store'))">
<div>
<InputLabel for="title" value="Title" />
<TextInput id="title" v-model="form.title" type="text" class="mt-1 block w-full" autofocus/>
<InputError class="mt-2" :message="form.errors.title" />
</div>
<div class="mt-4">
<InputLabel for="body" value="Body" />
<TextInput id="body" v-model="form.body" type="text" class="mt-1 block w-full"/>
<InputError class="mt-2" :message="form.errors.body" />
</div>
<div class="flex items-center justify-end mt-4">
<PrimaryButton type="submit" class="ml-4" :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
Create Post
</PrimaryButton>
</div>
</form>
</div>
</div>
</AuthenticatedLayout>
</template>
And then, insert following code into Edit.vue:
<script setup>
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue'
import InputError from '@/Components/InputError.vue'
import InputLabel from '@/Components/InputLabel.vue'
import TextInput from '@/Components/TextInput.vue'
import PrimaryButton from '@/Components/PrimaryButton.vue'
import { Head, useForm } from '@inertiajs/vue3'
const props = defineProps({
post: Object,
})
const form = useForm({
title: props.post.title,
body: props.post.body,
})
</script>
<template>
<Head title="Posts" />
<AuthenticatedLayout>
<h1 class="pl-6 py-2 text-3xl font-bold bg-gray-500 text-white">Posts</h1>
<div class="min-h-screen flex flex-col sm:justify-start items-center pt-6 sm:pt-0 bg-gray-100">
<div class="w-full sm:max-w-md mt-6 px-6 py-4 bg-white shadow-md overflow-hidden sm:rounded-lg">
<form @submit.prevent="form.put(route('posts.update', post.id))">
<div>
<InputLabel for="title" value="Title" />
<TextInput id="title" v-model="form.title" type="text" class="mt-1 block w-full" autofocus/>
<InputError class="mt-2" :message="form.errors.title" />
</div>
<div class="mt-4">
<InputLabel for="body" value="Body" />
<TextInput id="body" v-model="form.body" type="text" class="mt-1 block w-full"/>
<InputError class="mt-2" :message="form.errors.body" />
</div>
<div class="flex items-center justify-end mt-4">
<PrimaryButton type="submit" class="ml-4" :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
Update Post
</PrimaryButton>
</div>
</form>
</div>
</div>
</AuthenticatedLayout>
</template>
Finally, compile client side assets (JS and CSS files) by executing following command:
npm run build
Run PHP dev server by executing:
php artisan serv
Enter following URL in the browser and test CRUD functions (don’t forget to create a user login first):
localhost:8000/posts