### Step-by-Step Guide to Building a Laravel + Vue CRUD Application (Using Inertia.js)
This guide assumes you're using **Laravel 11** (or 10+) with **Inertia.js and Vue 3** for a modern, integrated setup. Inertia.js eliminates the need for a separate API, making it simpler for full-stack apps. We'll create a basic CRUD for managing "Posts" (with `title` and `body` fields).
#### Prerequisites
- PHP 8.2+, Composer, Node.js/npm, and a database (e.g., MySQL/SQLite).
- Run these in your terminal:
```
composer create-project laravel/laravel my-crud-app
cd my-crud-app
composer require inertiajs/inertia-laravel
php artisan inertia:middleware
# Add the middleware to app/Http/Kernel.php in $middlewareGroups['web']
npm install @inertiajs/vue3 vue
php artisan breeze:install vue --inertia # This scaffolds auth and Inertia setup
npm install && npm run dev
php artisan migrate
```
- Update `.env` for your database connection.
- Start the app: `php artisan serve` and open `http://127.0.0.1:8000`.
Now, let's build the CRUD step by step. I'll provide the full code for each relevant file.
#### Step 1: Create the Migration
Run: `php artisan make:migration create_posts_table`
File: `database/migrations/xxxx_xx_xx_xxxxxx_create_posts_table.php` (replace xxxx with the timestamp)
```php
<?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');
}
};
```
Run: `php artisan migrate`
#### Step 2: Create the Model
Run: `php artisan make:model Post`
File: `app/Models/Post.php`
```php
<?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'];
}
```
#### Step 3: Create the Controller
Run: `php artisan make:controller PostController --resource`
File: `app/Http/Controllers/PostController.php`
```php
<?php
namespace App\Http\Controllers;
use App\Models\Post;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
class PostController extends Controller
{
public function index(): Response
{
return Inertia::render('Posts/Index', [
'posts' => Post::all()
]);
}
public function create(): Response
{
return Inertia::render('Posts/Create');
}
public function store(Request $request): RedirectResponse
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'body' => 'required|string',
]);
Post::create($validated);
return redirect(route('posts.index'));
}
public function show(Post $post): Response
{
return Inertia::render('Posts/Show', [
'post' => $post
]);
}
public function edit(Post $post): Response
{
return Inertia::render('Posts/Edit', [
'post' => $post
]);
}
public function update(Request $request, Post $post): RedirectResponse
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'body' => 'required|string',
]);
$post->update($validated);
return redirect(route('posts.index'));
}
public function destroy(Post $post): RedirectResponse
{
$post->delete();
return redirect(route('posts.index'));
}
}
```
#### Step 4: Define Routes
File: `routes/web.php` (add this inside the file, after existing routes)
```php
use App\Http\Controllers\PostController;
// ... other routes ...
Route::resource('posts', PostController::class)->middleware(['auth', 'verified']);
```
This protects the CRUD routes with authentication (from Breeze).
#### Step 5: Create Vue Pages/Components
Inertia uses a `Pages` folder for views. Create a `Posts` subfolder in `resources/js/Pages/`.
##### 5.1: Index Page (List Posts)
File: `resources/js/Pages/Posts/Index.vue`
```vue
<script setup>
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
import { Head, Link } from '@inertiajs/vue3';
defineProps({
posts: Array,
});
</script>
<template>
<Head title="Posts" />
<AuthenticatedLayout>
<template #header>
<h2 class="font-semibold text-xl text-gray-800 leading-tight">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="route('posts.create')" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">Create Post</Link>
<table class="min-w-full mt-4">
<thead>
<tr>
<th>Title</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="post in posts" :key="post.id">
<td>{{ post.title }}</td>
<td>
<Link :href="route('posts.show', post.id)" class="text-blue-500">View</Link>
<Link :href="route('posts.edit', post.id)" class="text-green-500 ml-2">Edit</Link>
<button @click="destroy(post)" class="text-red-500 ml-2">Delete</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</AuthenticatedLayout>
</template>
<script>
export default {
methods: {
destroy(post) {
if (confirm('Are you sure?')) {
this.$inertia.delete(route('posts.destroy', post.id));
}
}
}
}
</script>
```
##### 5.2: Create Page
File: `resources/js/Pages/Posts/Create.vue`
```vue
<script setup>
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
import { Head, useForm } from '@inertiajs/vue3';
const form = useForm({
title: '',
body: '',
});
const submit = () => {
form.post(route('posts.store'));
};
</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-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">
<form @submit.prevent="submit">
<div>
<label for="title">Title</label>
<input id="title" v-model="form.title" type="text" class="mt-1 block w-full" />
<div v-if="form.errors.title" class="text-red-500">{{ form.errors.title }}</div>
</div>
<div class="mt-4">
<label for="body">Body</label>
<textarea id="body" v-model="form.body" class="mt-1 block w-full"></textarea>
<div v-if="form.errors.body" class="text-red-500">{{ form.errors.body }}</div>
</div>
<button type="submit" class="mt-4 bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" :disabled="form.processing">Create</button>
</form>
</div>
</div>
</div>
</div>
</AuthenticatedLayout>
</template>
```
##### 5.3: Edit Page
File: `resources/js/Pages/Posts/Edit.vue`
```vue
<script setup>
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
import { Head, useForm } from '@inertiajs/vue3';
defineProps({
post: Object,
});
const form = useForm({
title: post.title,
body: post.body,
});
const submit = () => {
form.put(route('posts.update', 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-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">
<form @submit.prevent="submit">
<div>
<label for="title">Title</label>
<input id="title" v-model="form.title" type="text" class="mt-1 block w-full" />
<div v-if="form.errors.title" class="text-red-500">{{ form.errors.title }}</div>
</div>
<div class="mt-4">
<label for="body">Body</label>
<textarea id="body" v-model="form.body" class="mt-1 block w-full"></textarea>
<div v-if="form.errors.body" class="text-red-500">{{ form.errors.body }}</div>
</div>
<button type="submit" class="mt-4 bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" :disabled="form.processing">Update</button>
</form>
</div>
</div>
</div>
</div>
</AuthenticatedLayout>
</template>
```
##### 5.4: Show Page (Optional, for viewing a single post)
File: `resources/js/Pages/Posts/Show.vue`
```vue
<script setup>
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
import { Head } from '@inertiajs/vue3';
defineProps({
post: Object,
});
</script>
<template>
<Head title="View Post" />
<AuthenticatedLayout>
<template #header>
<h2 class="font-semibold text-xl text-gray-800 leading-tight">View Post</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">
<h3 class="text-lg font-bold">{{ post.title }}</h3>
<p class="mt-2">{{ post.body }}</p>
</div>
</div>
</div>
</div>
</AuthenticatedLayout>
</template>
```
#### Step 6: Update Inertia Bootstrap (if needed)
File: `resources/js/app.js` (Breeze usually sets this up, but ensure it includes Vue and Inertia)
```javascript
import './bootstrap';
import '../css/app.css';
import { createApp, h } from 'vue';
import { createInertiaApp } from '@inertiajs/vue3';
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
import { ZiggyVue } from '../../vendor/tightenco/ziggy/dist/vue.es';
const appName = import.meta.env.VITE_APP_NAME || 'Laravel';
createInertiaApp({
title: (title) => `${title} - ${appName}`,
resolve: (name) => resolvePageComponent(`./Pages/${name}.vue`, import.meta.glob('./Pages/**/*.vue')),
setup({ el, App, props, plugin }) {
return createApp({ render: () => h(App, props) })
.use(plugin)
.use(ZiggyVue)
.mount(el);
},
progress: {
color: '#4B5563',
},
});
```
#### Step 7: Compile and Test
- Run `npm run dev` (or `npm run build` for production).
- Log in (Breeze provides auth at `/register` and `/login`).
- Visit `/posts` to see the CRUD in action.
This setup uses Tailwind CSS (from Breeze) for styling. For validation errors, Inertia handles them automatically. If you prefer the traditional Vue SPA without Inertia, let me know—I can provide that instead! If you encounter issues, check the console or Laravel logs.
0 Comments