Laravel 12 + Inertia + React Full-Stack Tutorial
In this tutorial, we will build a simple CRUD application to manage posts using Laravel 12 for the backend and React JS for the frontend using Inertia.js.
What You'll Build:
A Post CRUD app (Create, Read, Update, Delete) using:
Laravel 12 (Backend)
Inertia.js (Bridge between Laravel and React)
React.js 18 (Frontend)
Requirements
Make sure you have the following installed:
-
PHP 8.2+
-
Composer
-
Node.js + npm
-
Laravel CLI
-
MySQL/PostgreSQL
-
A web browser (Chrome, Firefox, etc.)
Step 1: Install Laravel 12
composer create-project laravel/laravel laravel-react-crud
Update .env
to connect with your database:
DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=react_crud DB_USERNAME=root DB_PASSWORD=
Step 2: Install Breeze with Inertia.js + React
composer require laravel/breeze --dev php artisan breeze:install react npm install npm run dev
This installs Breeze with Inertia + React stack and sets up default auth scaffolding.
Step 3: Create Post Model and Migration
php artisan make:model Post -mcr
In database/migrations/xxxx_create_posts_table.php,
Define the schema:
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('body');
$table->timestamps();
});
Run the migration:
php artisan migrate
Update the model app/Models/Post.php
:
protected $fillable = ['title', 'body'];
Step 4: Create Post Controller
Add logic to app/Http/Controllers/PostController.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)
{
$request->validate([
'title' => 'required',
'body' => 'required',
]);
Post::create($request->all());
return redirect()->route('posts.index');
}
public function edit(Post $post)
{
return Inertia::render('Posts/Edit', [
'post' => $post
]);
}
public function update(Request $request, Post $post)
{
$request->validate([
'title' => 'required',
'body' => 'required',
]);
$post->update($request->all());
return redirect()->route('posts.index');
}
public function destroy(Post $post)
{
$post->delete();
return redirect()->route('posts.index');
}
}
Step 5: Define Routes
In routes/web.php
:
use App\Http\Controllers\PostController;
Route::middleware(['auth', 'verified'])->group(function () {
Route::resource('posts', PostController::class);
});
Step 6: Create React Components
Create a folder:
mkdir resources/js/Pages/Posts
📄 resources/js/Pages/Posts/Index.jsx
import React, { useState } from 'react'; import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout'; import { Head, Link, router } from '@inertiajs/react'; export default function Index({ auth, posts }) { const [showDeleteModal, setShowDeleteModal] = useState(false); const [postToDelete, setPostToDelete] = useState(null); const openDeleteModal = (id) => { setPostToDelete(id); setShowDeleteModal(true); }; const cancelDelete = () => { setShowDeleteModal(false); setPostToDelete(null); }; const confirmDelete = () => { if (postToDelete) { router.delete(`/posts/${postToDelete}`, { onSuccess: () => { setShowDeleteModal(false); setPostToDelete(null); }, }); } }; return ( <AuthenticatedLayout user={auth.user}> <Head title="Posts" /> <div className="py-12"> <div className="max-w-7xl mx-auto sm:px-6 lg:px-8"> <div className="bg-white overflow-hidden shadow-sm sm:rounded-lg p-6"> <div className="flex justify-between items-center mb-4"> <h2 className="text-xl font-bold">Post List</h2> <Link href="/posts/create" className="bg-blue-600 text-white text-sm px-2.5 py-1.5 rounded hover:bg-blue-700" > ➕ Create </Link> </div> <table className="w-full table-auto border"> <thead> <tr className="bg-gray-100 text-left"> <th className="border p-2 w-[8%]">No</th> <th className="border p-2">Title</th> <th className="border p-2">Body</th> <th className="border p-2 text-center w-[20%]">Actions</th> </tr> </thead> <tbody> {posts.length === 0 ? ( <tr> <td colSpan="4" className="border p-4 text-center text-gray-500"> No posts available. </td> </tr> ) : ( posts.map((post, index) => ( <tr key={post.id} className="hover:bg-gray-50"> <td className="border p-2 text-center">{index + 1}</td> <td className="border p-2">{post.title}</td> <td className="border p-2">{post.body}</td> <td className="border p-2 text-center space-x-2"> <Link href={`/posts/${post.id}/edit`} className="text-xs text-white bg-blue-600 hover:bg-blue-700 px-2 py-1 rounded" > ✏️ Edit </Link> <button onClick={() => openDeleteModal(post.id)} className="text-xs text-white bg-red-600 hover:bg-red-700 px-2 py-1 rounded" > 🗑️ Delete </button> </td> </tr> )) )} </tbody> </table> </div> </div> </div> {/* Delete Confirmation Modal */} {showDeleteModal && ( <div className="fixed inset-0 flex items-center justify-center z-50 bg-black bg-opacity-50"> <div className="bg-white rounded-lg shadow-md w-full max-w-sm p-6"> <h2 className="text-lg font-semibold text-gray-800 mb-4 text-center"> Confirm Deletion </h2> <p className="text-gray-600 mb-6 text-center"> Are you sure you want to delete this post? </p> <div className="flex justify-center space-x-2"> <button onClick={cancelDelete} className="bg-gray-300 hover:bg-gray-400 text-gray-800 font-semibold py-1 px-4 rounded" > Cancel </button> <button onClick={confirmDelete} className="bg-red-500 hover:bg-red-700 text-white font-semibold py-1 px-4 rounded" > Yes, Delete </button> </div> </div> </div> )} </AuthenticatedLayout> ); }
➕ resources/js/Pages/Posts/Create.jsx
import React from 'react'; import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout'; import { Head, Link, useForm } from '@inertiajs/react'; export default function Create({ auth }) { const { data, setData, post, processing, errors } = useForm({ title: '', body: '', }); const submit = (e) => { e.preventDefault(); post('/posts'); }; return ( <AuthenticatedLayout user={auth.user} header={ <h2 className="font-semibold text-xl text-gray-800 leading-tight"> Create Posts </h2> } > <Head title="Create Post" /> <div className="py-12"> <div className="max-w-4xl mx-auto px-2 sm:px-4 lg:px-5"> <div className="bg-white overflow-hidden shadow-sm sm:rounded-lg p-6"> {/* Back Button */} <Link href="/posts" className="inline-flex items-center gap-2 px-4 py-2 bg-gray-200 text-gray-700 text-sm font-medium rounded hover:bg-gray-300" > 🔙 <span>Back</span> </Link> {/* Form */} <form onSubmit={submit} className="space-y-6 mt-6"> {/* Title Field */} <div> <label className="block text-sm font-medium text-gray-700 mb-1">Title</label> <input type="text" value={data.title} onChange={(e) => setData('title', e.target.value)} className="w-full border border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring focus:ring-blue-200" /> {errors.title && <p className="text-red-500 text-sm mt-1">{errors.title}</p>} </div> {/* Body Field */} <div> <label className="block text-sm font-medium text-gray-700 mb-1">Body</label> <textarea value={data.body} onChange={(e) => setData('body', e.target.value)} className="w-full border border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring focus:ring-blue-200" /> {errors.body && <p className="text-red-500 text-sm mt-1">{errors.body}</p>} </div> {/* Submit Button */} <button type="submit" disabled={processing} className={`bg-green-600 text-white text-sm px-4 py-2 rounded hover:bg-green-700 transition ${ processing ? 'opacity-50 cursor-not-allowed' : '' }`} > ✅ Submit </button> </form> </div> </div> </div> </AuthenticatedLayout> ); }
✏️ resources/js/Pages/Posts/Edit.jsx
import React from 'react'; import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout'; import { Head, Link, useForm } from '@inertiajs/react'; export default function Edit({ auth, post }) { const { data, setData, put, processing, errors } = useForm({ title: post.title || '', body: post.body || '', }); const submit = (e) => { e.preventDefault(); put(`/posts/${post.id}`); }; return ( <AuthenticatedLayout user={auth.user} header={ <h2 className="font-semibold text-xl text-gray-800 leading-tight"> Edit Post </h2> } > <Head title="Edit Post" /> <div className="py-12"> <div className="max-w-4xl mx-auto px-2 sm:px-4 lg:px-5"> <div className="bg-white overflow-hidden shadow-sm sm:rounded-lg p-6"> <Link href="/posts" className="inline-flex items-center px-4 py-2 bg-gray-200 text-gray-700 text-sm font-medium rounded hover:bg-gray-300" > 🔙 Back </Link> <form onSubmit={submit} className="space-y-4 mt-6"> <div> <label className="block text-sm font-medium text-gray-700 mb-1">Title</label> <input type="text" value={data.title} onChange={(e) => setData('title', e.target.value)} className="w-full border border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring focus:ring-blue-200" /> {errors.title && <p className="text-red-500 text-sm mt-1">{errors.title}</p>} </div> <div> <label className="block text-sm font-medium text-gray-700 mb-1">Body</label> <textarea value={data.body} onChange={(e) => setData('body', e.target.value)} className="w-full border border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring focus:ring-blue-200" /> {errors.body && <p className="text-red-500 text-sm mt-1">{errors.body}</p>} </div> <button type="submit" disabled={processing} className={`bg-blue-600 text-white text-sm px-4 py-2 rounded hover:bg-blue-700 transition ${ processing ? 'opacity-50 cursor-not-allowed' : '' }`} > 💾 Update </button> </form> </div> </div> </div> </AuthenticatedLayout> ); }
Step 7: Add Posts to Navigation Menu
Open resources/js/Layouts/AuthenticatedLayout.jsx.
, and inside your navigation area:
<NavLink href={route('posts.index')} active={route().current('posts.*')}> Posts </NavLink>
Final Touch
Run the app:
php artisan serve npm run dev
Visit: http://localhost:8000/posts
(after login/register)
Summary
You’ve built a full Post CRUD using:
-
Laravel 12 as the backend
-
Inertia.js to connect the back-end and front-end
-
React 18 for UI components