Contents
One of the most fundamental parts of an application is authentication and CRUD (Create-Read-Update-Delete) Operation. In this tutorial, we will be showing you how to make a CRUD App with authentication using Laravel 11, React, and Inertia. We will be using the Laravel Breeze, it is a good starting point when making a Laravel application.
Laravel is a free, open-source PHP Web Framework intended for the development of web applications following the MVC (Model-View-Controller) architectural pattern. Laravel is designed to make developing web apps faster and easier by using built-in features.
Laravel Breeze gives you a simple and minimal implementation of login, registration, password reset, email verification, and password confirmation which are part of Laravel’s authentication features.
React or also called React.js or Reactjs is a free and open-source JavaScript library used for building user interfaces(UI). It is one of the most popular JavaScript libraries for building the front end. React is created by Facebook and maintained by Facebook.
Inertia.js is a framework designed to create modern single-page applications (SPAs) without the complexity of a fully client-side setup. It allows developers to build SPAs using server-side frameworks such as Laravel, while still leveraging front-end technologies like Vue.js, React, or Svelte.
Prerequisite:
- Composer
- PHP >= 8.2
- Node >= 18
Step 1: Install Laravel 11
First, select a folder you want Laravel to be installed then execute this command on Terminal or CMD to install Laravel 11:
Install via composer:
composer create-project laravel/laravel laravel-11-react-inertia
Install via Laravel Installer:
laravel new laravel-11-react-inertia
Step 2: Set Database Configuration
Open the .env file and set the database configuration:
.env
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=your database name(laravel_11_react_inertia)
DB_USERNAME=your database username(root)
DB_PASSWORD=your database password(root)
Step 3: Install Laravel Breeze
Then install the Laravel Breeze package
composer require laravel/breeze --dev
After installing packages, run the artisan command breeze:install to publish the authentication views, routes, controller, and other resources to the project.
php artisan breeze:install
We will be choosing React with Inertia as stack and testing will be PHPUnit:
After the installation, run these commands to compile the assets.
php artisan migrate
npm install
npm run dev
Step 4: Create a Model with Migration
A model is a class that represents a table on a database.
Migration is like a version of your database.
Run this command on Terminal or CMD:
php artisan make:model Post --migration
After running this command you will find a file in this path “database/migrations” and update the code in that file.
<?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('posts', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('content');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('posts');
}
};
Run the migration:
php artisan migrate
Step 5: Create Controller
We will now create our controller. The controller will be responsible for handling HTTP incoming requests.
Run this command to create a Resource Controller:
php artisan make:controller PostController --resource
Update PostController.php with CRUD methods:
app\Http\Controllers\PostController.php
<?php
namespace App\Http\Controllers;
use App\Models\Post;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Redirect;
use Inertia\Inertia;
class PostController extends Controller
{
public function index()
{
$posts = Post::all();
return Inertia::render('Posts/Index', ['posts' => $posts]);
}
public function create()
{
return Inertia::render('Posts/Create');
}
public function store(Request $request)
{
$request->validate([
'title' => 'required',
'content' => 'required',
]);
$post = new Post();
$post->title = $request->title;
$post->content = $request->content;
$post->save();
return Redirect::route('posts.index')->with('success', 'Post created successfully.');
}
public function show(Post $post)
{
return Inertia::render('Posts/Show', ['post' => $post]);
}
public function edit(Post $post)
{
return Inertia::render('Posts/Edit', ['post' => $post]);
}
public function update(Request $request, Post $post)
{
$request->validate([
'title' => 'required',
'content' => 'required',
]);
$post->title = $request->title;
$post->content = $request->content;
$post->save();
return Redirect::route('posts.index')->with('success', 'Post updated successfully.');
}
public function destroy(Post $post)
{
$post->delete();
return Redirect::route('posts.index')->with('success', 'Post deleted successfully.');
}
}
Step 6: Define Routes
Update web.php to include resource routes for the PostController:
routes\web.php
<?php
use App\Http\Controllers\ProfileController;
use App\Http\Controllers\PostController;
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;
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::resource('posts', PostController::class)
->names('posts');
});
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');
});
require __DIR__.'/auth.php';
Step 7: Update Inertia Middleware
We will be adding flash data on Inertia middleware. we will be using the flash data to display notification
app\Http\Middleware\HandleInertiaRequests.php
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Request;
use Inertia\Middleware;
class HandleInertiaRequests extends Middleware
{
/**
* The root template that is loaded on the first page visit.
*
* @var string
*/
protected $rootView = 'app';
/**
* Determine the current asset version.
*/
public function version(Request $request): string|null
{
return parent::version($request);
}
/**
* Define the props that are shared by default.
*
* @return array<string, mixed>
*/
public function share(Request $request): array
{
return [
...parent::share($request),
'auth' => [
'user' => $request->user(),
],
'flash' => [
'success' => fn () => $request->session()->get('success')
],
];
}
}
Step 8: Create React Components
Create a textarea component:
resources\js\Components\TextareaInput.jsx
import { forwardRef, useEffect, useRef } from 'react';
export default forwardRef(function TextareaInput({ className = '', isFocused = false, ...props }, ref) {
const input = ref ? ref : useRef();
useEffect(() => {
if (isFocused) {
input.current.focus();
}
}, []);
return (
<textarea
{...props}
className={
'border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm ' +
className
}
ref={input}
/>
);
});
Create the necessary React components for CRUD operations pages.
resources\js\Pages\Posts\Create.jsx
import React, { useEffect, useState } from 'react';
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { Head, Link, useForm } from '@inertiajs/react';
import InputError from '@/Components/InputError';
import InputLabel from '@/Components/InputLabel';
import PrimaryButton from '@/Components/PrimaryButton';
import TextInput from '@/Components/TextInput';
import { Transition } from '@headlessui/react';
import SecondaryButton from '@/Components/SecondaryButton';
import TextareaInput from '@/Components/TextareaInput';
export default function Create({ auth }) {
const { data, setData, post, errors, processing, recentlySuccessful } = useForm({
title: '',
content:'',
});
const submit = (e) => {
e.preventDefault();
post(route('posts.store'));
};
return (
<AuthenticatedLayout
user={auth.user}
header={<h2 className="font-semibold text-xl text-gray-800 leading-tight">Create Post</h2>}
>
<Head title="Create Post" />
<div className="py-12">
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6">
<form onSubmit={submit} className="mt-6 space-y-6">
<div>
<InputLabel htmlFor="title" value="Title" />
<TextInput
id="title"
className="mt-1 block w-full"
value={data.title}
onChange={(e) => setData('title', e.target.value)}
required
isFocused
autoComplete="title"
/>
<InputError className="mt-2" message={errors.title} />
</div>
<div>
<InputLabel htmlFor="content" value="Content" />
<TextareaInput
id="content"
className="mt-1 block w-full"
value={data.content}
onChange={(e) => setData('content', e.target.value)}
required
autoComplete="content"
rows={10}
/>
<InputError className="mt-2" message={errors.content} />
</div>
<div className="flex items-center gap-4">
<Link href={route('posts.index')}>
<SecondaryButton disabled={processing}>Cancel</SecondaryButton>
</Link>
<PrimaryButton disabled={processing}>Save</PrimaryButton>
<Transition
show={recentlySuccessful}
enter="transition ease-in-out"
enterFrom="opacity-0"
leave="transition ease-in-out"
leaveTo="opacity-0"
>
<p className="text-sm text-gray-600">Saved.</p>
</Transition>
</div>
</form>
</div>
</div>
</AuthenticatedLayout>
);
}
resources\js\Pages\Posts\Edit.jsx
import React, { useEffect, useState } from 'react';
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { Head, Link, useForm } from '@inertiajs/react';
import InputError from '@/Components/InputError';
import InputLabel from '@/Components/InputLabel';
import PrimaryButton from '@/Components/PrimaryButton';
import TextInput from '@/Components/TextInput';
import { Transition } from '@headlessui/react';
import SecondaryButton from '@/Components/SecondaryButton';
import TextareaInput from '@/Components/TextareaInput';
export default function Edit({ auth, post }) {
const { data, setData, patch, errors, processing, recentlySuccessful } = useForm({
title: post.title,
content: post.content,
});
const submit = (e) => {
e.preventDefault();
patch(route('posts.update', {id:post.id}));
};
return (
<AuthenticatedLayout
user={auth.user}
header={<h2 className="font-semibold text-xl text-gray-800 leading-tight">Edit Post</h2>}
>
<Head title="Create Post" />
<div className="py-12">
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6">
<form onSubmit={submit} className="mt-6 space-y-6">
<div>
<InputLabel htmlFor="title" value="Title" />
<TextInput
id="title"
className="mt-1 block w-full"
value={data.title}
onChange={(e) => setData('title', e.target.value)}
required
isFocused
autoComplete="title"
/>
<InputError className="mt-2" message={errors.title} />
</div>
<div>
<InputLabel htmlFor="content" value="Content" />
<TextareaInput
id="content"
className="mt-1 block w-full"
value={data.content}
onChange={(e) => setData('content', e.target.value)}
required
autoComplete="content"
rows={10}
/>
<InputError className="mt-2" message={errors.content} />
</div>
<div className="flex items-center gap-4">
<Link href={route('posts.index')}>
<SecondaryButton disabled={processing}>Cancel</SecondaryButton>
</Link>
<PrimaryButton disabled={processing}>Save</PrimaryButton>
<Transition
show={recentlySuccessful}
enter="transition ease-in-out"
enterFrom="opacity-0"
leave="transition ease-in-out"
leaveTo="opacity-0"
>
<p className="text-sm text-gray-600">Saved.</p>
</Transition>
</div>
</form>
</div>
</div>
</AuthenticatedLayout>
);
}
resources\js\Pages\Posts\Index.jsx
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { Head, Link, usePage } from '@inertiajs/react';
import NavLink from '@/Components/NavLink';
import PrimaryButton from '@/Components/PrimaryButton';
import SecondaryButton from '@/Components/SecondaryButton';
import { useForm } from '@inertiajs/react';
import { useEffect } from 'react';
export default function Index({ auth, posts, message }) {
const { flash } = usePage().props
const {
delete: destroy,
processing,
} = useForm();
const deletePost = (id) => {
destroy(route('posts.destroy',{id:id}));
}
return (
<AuthenticatedLayout
user={auth.user}
header={<h2 className="font-semibold text-xl text-gray-800 leading-tight">Posts</h2>}
>
<Head title="Posts" />
<div className="py-12">
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6">
{flash.success && (
<div className="p-4 mb-4 text-sm text-green-800 rounded-lg bg-green-50 dark:bg-gray-800 dark:text-green-400" role="alert">
<span className="font-medium">{flash.success}</span>
</div>
)}
<NavLink href={route('posts.create')} active={route().current('posts.create')}>
<PrimaryButton>Create</PrimaryButton>
</NavLink>
<div className="relative overflow-x-auto">
<table className="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
<thead className="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
<tr>
<th scope="col" className="px-6 py-3">
Title
</th>
<th scope="col" className="px-6 py-3">
Content
</th>
<th scope="col" className="px-6 py-3">
Action
</th>
</tr>
</thead>
<tbody>
{posts.map(post => (
<tr key={post.id} className="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
<th scope="row" className="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
{post.title}
</th>
<td className="px-6 py-4">
<p className='w-52 truncate text-ellipsis overflow-hidden '>{post.content}</p>
</td>
<td className="px-6 py-4">
<Link href={route('posts.show',{id:post.id})}>
<SecondaryButton className='mr-1 mb-1 text-sky-400'>View</SecondaryButton>
</Link>
<Link href={route('posts.edit',{id:post.id})}>
<SecondaryButton className='mr-1 mb-1 text-green-400'>Edit</SecondaryButton>
</Link>
<SecondaryButton className='mr-1 mb-1 text-red-400' disabled={processing} onClick={()=>deletePost(post.id)}>Delete</SecondaryButton>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</AuthenticatedLayout>
);
}
resources\js\Pages\Posts\Show.jsx
import React from 'react';
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { Head, Link } from '@inertiajs/react';
export default function Show({ auth, post }) {
return (
<AuthenticatedLayout
user={auth.user}
header={<h2 className="font-semibold text-xl text-gray-800 leading-tight">View Post</h2>}
>
<Head title="Create Post" />
<div className="py-12">
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6">
<p className='text-3xl'>{post.title}</p>
<p>{post.content}</p>
</div>
</div>
</AuthenticatedLayout>
);
}
Update the AuthenticatedLayout.jsx file:
resources\js\Layouts\AuthenticatedLayout.jsx
import { useState } from 'react';
import ApplicationLogo from '@/Components/ApplicationLogo';
import Dropdown from '@/Components/Dropdown';
import NavLink from '@/Components/NavLink';
import ResponsiveNavLink from '@/Components/ResponsiveNavLink';
import { Link } from '@inertiajs/react';
export default function Authenticated({ user, header, children }) {
const [showingNavigationDropdown, setShowingNavigationDropdown] = useState(false);
return (
<div className="min-h-screen bg-gray-100">
<nav className="bg-white border-b border-gray-100">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between h-16">
<div className="flex">
<div className="shrink-0 flex items-center">
<Link href="/">
<ApplicationLogo className="block h-9 w-auto fill-current text-gray-800" />
</Link>
</div>
<div className="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex">
<NavLink href={route('dashboard')} active={route().current('dashboard')}>
Dashboard
</NavLink>
<NavLink href={route('posts.index')} active={route().current('posts.index')}>
Posts
</NavLink>
</div>
</div>
<div className="hidden sm:flex sm:items-center sm:ms-6">
<div className="ms-3 relative">
<Dropdown>
<Dropdown.Trigger>
<span className="inline-flex rounded-md">
<button
type="button"
className="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 bg-white hover:text-gray-700 focus:outline-none transition ease-in-out duration-150"
>
{user.name}
<svg
className="ms-2 -me-0.5 h-4 w-4"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</button>
</span>
</Dropdown.Trigger>
<Dropdown.Content>
<Dropdown.Link href={route('profile.edit')}>Profile</Dropdown.Link>
<Dropdown.Link href={route('logout')} method="post" as="button">
Log Out
</Dropdown.Link>
</Dropdown.Content>
</Dropdown>
</div>
</div>
<div className="-me-2 flex items-center sm:hidden">
<button
onClick={() => setShowingNavigationDropdown((previousState) => !previousState)}
className="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 focus:text-gray-500 transition duration-150 ease-in-out"
>
<svg className="h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
<path
className={!showingNavigationDropdown ? 'inline-flex' : 'hidden'}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M4 6h16M4 12h16M4 18h16"
/>
<path
className={showingNavigationDropdown ? 'inline-flex' : 'hidden'}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
</div>
</div>
<div className={(showingNavigationDropdown ? 'block' : 'hidden') + ' sm:hidden'}>
<div className="pt-2 pb-3 space-y-1">
<ResponsiveNavLink href={route('dashboard')} active={route().current('dashboard')}>
Dashboard
</ResponsiveNavLink>
</div>
<div className="pt-4 pb-1 border-t border-gray-200">
<div className="px-4">
<div className="font-medium text-base text-gray-800">{user.name}</div>
<div className="font-medium text-sm text-gray-500">{user.email}</div>
</div>
<div className="mt-3 space-y-1">
<ResponsiveNavLink href={route('profile.edit')}>Profile</ResponsiveNavLink>
<ResponsiveNavLink method="post" href={route('logout')} as="button">
Log Out
</ResponsiveNavLink>
</div>
</div>
</div>
</nav>
{header && (
<header className="bg-white shadow">
<div className="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">{header}</div>
</header>
)}
<main>{children}</main>
</div>
);
}
Step 9: Run the Application
After finishing the steps above, you can now run your application by executing the code below:
php artisan serve
After successfully running your app, open this URL in your browser:
http://127.0.0.1:8000/
Screenshots:
Post index page
Post create page
Post edit page
Post view page