How To Make A CRUD App With Authentication Using Laravel 11, React And Inertia

How To Make A CRUD App With Authentication Using Laravel 11, React And Inertia

Avatar photoPosted by

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:

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:

image 57 Binaryboxtuts

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

How To Make A CRUD App With Authentication Using Laravel 11, React And Inertia

Post create page

How To Make A CRUD App With Authentication Using Laravel 11, React And Inertia

Post edit page

How To Make A CRUD App With Authentication Using Laravel 11, React And Inertia

Post view page

How To Make A CRUD App With Authentication Using Laravel 11, React And Inertia