Laravel 11 Inertia CRUD with React TypeScript Example

Learn how to build a CRUD application with Laravel, Inertia.js, and React.js. This tutorial will walk you through creating a modern, interactive SPA with Laravel as the backend, Inertia.js for seamless page transitions, and React.js for dynamic front-end features. Let’s dive in and create a powerful web app from scratch!

Step 1: Install Laravel and Configure Database

Installing a fresh new laravel application.

composer create-project laravel/laravel inertia-react

To connect your Laravel application to a database, you’ll need to update the .env configuration file with your database credentials. Open the .env file and enter your database details as follows:

.env
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=database_name
DB_USERNAME=database_user_name
DB_PASSWORD=database_password

Step 2: Install Breeze and Set Up Inertia.js with React

Install laravel breeze via composer.

composer require laravel/breeze --dev

Next, run below command.

php artisan breeze:install
  Which Breeze stack would you like to install?
  Blade with Alpine ................................ blade
  Livewire (Volt Class API) with Alpine ............ livewire
  Livewire (Volt Functional API) with Alpine ........ livewire-functional
  React with Inertia ........................ react
  Vue with Inertia ............................... vue
  API only .................... api
 react

Next, You need to select typescript.

Step 3: Create Post Model and Resource Controller

To create a Post model, migration, and controller, run the following command in your terminal:

php artisan make:model Post -mcr
  • -m generates a migration file.
  • -c generates a controller.
  • -r adds resource methods to the controller.

This command will create:

  1. A Post model in app/Models/Post.php.
  2. A migration file in database/migrations/ for the posts table.
  3. A resource controller in app/Http/Controllers/PostController.php with default CRUD methods.
create_posts_table.php
<?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('description');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('posts');
    }
};
PHP
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    protected $fillable = [
        'title',
        'description'
    ];
    
    use HasFactory;
}

Step 4: Implement Form Request Validation, Update Controller, and Define Routes

To handle validation for creating and updating posts, you need to create a form request. Follow these steps:

  1. Generate the Form Request:
    • Run the following command to create a StorePostRequest class:
php artisan make:request StorePostRequest
  1. Define Validation Rules:
    • Open the newly created PostRequest file and add your validation rules in the rules method. For example:
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class StorePostRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return true; // Adjust this based on your authorization logic
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        return [
            'title' => 'required|string|max:255',
            'description' => 'required|string'
        ];
    }
}
  1. Use the Form Request in Your Controller: also add Redirect, Inertia.
PostController.php
<?php

namespace App\Http\Controllers;

use App\Models\Post;
use Illuminate\Http\Request;
use App\Http\Requests\StorePostRequest;
use Illuminate\Support\Facades\Redirect;
use Inertia\Inertia;

class PostController extends Controller
{
    /**
     * Display a listing of the resource.
     */
    public function index()
    {
        $posts = Post::latest()->get();

        return Inertia::render('Post/Index', ['posts' => $posts]);
    }

    /**
     * Show the form for creating a new resource.
     */
    public function create()
    {
        return Inertia::render('Post/Create');
    }

    /**
     * Store a newly created resource in storage.
     */
    public function store(StorePostRequest  $request)
    {
        Post::create(
            $request->validated()
        );
        return Redirect::route('posts.index');

    }

    /**
     * Display the specified resource.
     */
    public function show(Post $post)
    {
        //
    }

    /**
     * Show the form for editing the specified resource.
     */
    public function edit(Post $post)
    {
        return Inertia::render('Post/Edit', [
            'post' => [
                'id' => $post->id,
                'title' => $post->title,
                'description' => $post->description
            ]
        ]);
    }

    /**
     * Update the specified resource in storage.
     */
    public function update(StorePostRequest $request, Post $post)
    {
        $post->update($request->validated());

        return Redirect::route('posts.index');
    }

    /**
     * Remove the specified resource from storage.
     */
    public function destroy(Post $post)
    {
        $post->delete();

        return Redirect::route('posts.index');
    }
}
  1. Define 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::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');
   
});
Route::resource('posts', PostController::class);
require __DIR__.'/auth.php';

Step 5: Create React.js Views for CRUD Operations

  • Create the Create View

How to Use Inertia Links in Laravel 11 with React

Generate the Create view for adding new posts:

resources/js/Pages/Post/Create.tsx
import React from "react";
import { Link, useForm } from "@inertiajs/react";
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { PageProps } from '@/types';

// Define the types for form data and errors
interface FormData {
    title: string;
    description: string;
}

interface FormErrors {
    title?: string;
    description?: string;
}

const Create = ({ auth }: PageProps) => {
    const { data, setData, errors, post } = useForm<FormData>({
        title: "",
        description: "",
    });

    // Handle form submission
    function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
        e.preventDefault();
        post(route("posts.store"));
    }

    return (
        <AuthenticatedLayout
            user={auth.user}
            header={<h2 className="font-semibold text-xl text-gray-800 leading-tight">Dashboard</h2>}
        >
                <div className="container mx-auto max-w-7xl">
                    <div>
                        <h1 className="mb-8 text-3xl font-bold mt-8">
                            <Link
                                href={route("posts.index")}
                                className="text-blue-600 font-bold hover:text-blue-700"
                            >
                                Posts
                            </Link>
                            <span className="font-medium text-blue-600"> / </span>
                            Create
                        </h1>
                    </div>
                    <div className="max-w-6xl p-8 bg-white rounded shadow">
                        <form name="createForm" onSubmit={handleSubmit}>
                            <div className="flex flex-col">
                                <div className="mb-4">
                                    <label htmlFor="title" className="block text-gray-700">Title</label>
                                    <input
                                        type="text"
                                        id="title"
                                        className="w-full px-4 py-2 border border-gray-300 rounded-md"
                                        name="title"
                                        value={data.title}
                                        onChange={(e) =>
                                            setData("title", e.target.value)
                                        }
                                    />
                                    {errors.title && (
                                        <span className="text-red-600 text-sm">
                                            {errors.title}
                                        </span>
                                    )}
                                </div>
                                <div className="mb-4">
                                    <label htmlFor="description" className="block text-gray-700">Description</label>
                                    <textarea
                                        id="description"
                                        className="w-full px-4 py-2 border border-gray-300 rounded-md"
                                        name="description"
                                        value={data.description}
                                        onChange={(e) =>
                                            setData("description", e.target.value)
                                        }
                                    />
                                    {errors.description && (
                                        <span className="text-red-600 text-sm">
                                            {errors.description}
                                        </span>
                                    )}
                                </div>
                            </div>
                            <div>
                                <button
                                    type="submit"
                                    className="px-3 py-1.5  text-white bg-blue-500 rounded hover:bg-blue-600"
                                >
                                    Save
                                </button>
                            </div>
                        </form>
                    </div>
                </div>
           
        </AuthenticatedLayout>
    );
};

export default Create;
inertia react create
  • Create the Index View

Tailwind CSS Simple Table Example

Generate the Index view for listing posts:

resources/js/Pages/Post/Index.tsx
import React from "react";
import { Link, usePage, router } from "@inertiajs/react";
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { PageProps } from '@/types';

// Define the type for a single post
interface Post {
    id: number;
    title: string;
    description: string;
}

// Adjust the type to reflect the correct structure of posts
interface Posts {
    data: Post[];
}

const Index = ({ auth }: PageProps) => {
    const { posts } = usePage().props;
    const data: Post[] = posts; 
    console.log(posts); // Check the structure in the console

    // Function to handle delete action
    const handleDelete = (id: number) => {
        if (confirm("Are you sure you want to delete this post?")) {
            router.delete(route("posts.destroy", id));
        }
    };

    return (
        <AuthenticatedLayout
            user={auth.user}
            header={<h2 className="font-semibold text-xl text-gray-800 leading-tight">Dashboard</h2>}
        >
            <div className="container mx-auto max-w-7xl mt-4">
                <h1 className="mb-8 text-4xl font-bold text-center">Posts Index</h1>
                <div className="flex items-center justify-between mb-6">
                    <Link 
                        className="px-3 py-1.5 text-white bg-blue-500 rounded-md focus:outline-none"
                        href={route("posts.create")}
                    >
                        Create Post
                    </Link >
                </div>

                <div className="overflow-x-auto">
                    <table className="min-w-full bg-white">
                        <thead>
                            <tr>
                                <th className="px-4 py-2 bg-gray-200 text-gray-600 border-b border-gray-300 text-left text-sm uppercase font-semibold">
                                    #
                                </th>
                                <th className="px-4 py-2 bg-gray-200 text-gray-600 border-b border-gray-300 text-left text-sm uppercase font-semibold">
                                    Title
                                </th>
                                <th className="px-4 py-2 bg-gray-200 text-gray-600 border-b border-gray-300 text-left text-sm uppercase font-semibold">
                                    Description
                                </th>
                                <th className="px-4 py-2 bg-gray-200 text-gray-600 border-b border-gray-300 text-left text-sm uppercase font-semibold">
                                    Actions
                                </th>
                            </tr>
                        </thead>
                        <tbody>
                            {data.length > 0 ? (
                                data.map(({ id, title, description }) => (
                                    <tr key={id}>
                                        <td className="px-4 py-2 border-b border-gray-300">
                                            {id}
                                        </td>
                                        <td className="px-4 py-2 border-b border-gray-300">
                                            {title}
                                        </td>
                                        <td className="px-4 py-2 border-b border-gray-300">
                                            {description}
                                        </td>
                                        <td className="px-4 py-2 border-b border-gray-300">
                                            <Link 
                                                className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-1 px-2 rounded text-xs mr-1"
                                                href={route("posts.edit", id)}
                                            >
                                                Edit
                                            </Link>
                                            <button 
                                                className="bg-red-500 hover:bg-red-700 text-white font-bold py-1 px-2 rounded text-xs"
                                                onClick={() => handleDelete(id)} // Trigger delete function
                                            >
                                                Delete
                                            </button>
                                        </td>
                                    </tr>
                                ))
                            ) : (
                                <tr>
                                    <td
                                        className="px-4 py-2 border-b border-gray-300"
                                        colSpan="4"
                                    >
                                        No posts found.
                                    </td>
                                </tr>
                            )}
                        </tbody>
                    </table>
                </div>
            </div>
        </AuthenticatedLayout>
    );
};

export default Index;
inertia react index
  • Create the Edit View

Generate the Edit view for updating existing posts:

resources/js/Pages/Post/Edit.tsx
import React from "react";
import { Link, usePage, useForm, router  } from "@inertiajs/react";
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { PageProps } from '@/types';

// Define types for form data and errors
interface Post {
    id: number;
    title: string;
    description: string;
}

interface FormErrors {
    title?: string;
    description?: string;
}

const Edit = ({ auth }: PageProps) => {
    const { post }: { post: Post } = usePage().props;
    const { data, setData, put, errors } = useForm({
        title: post.title || "",
        description: post.description || "",
    });

    function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
        e.preventDefault();
        put(route("posts.update", post.id));
    }

    function destroy() {
        if (confirm("Are you sure you want to delete this post?")) {
            router.delete(route("posts.destroy", post.id));
        }
    }

    return (
        <AuthenticatedLayout
            user={auth.user}
            header={<h2 className="font-semibold text-xl text-gray-800 leading-tight">Dashboard</h2>}
        >
            <div className="container mx-auto max-w-7xl mt-8">
                <div>
                    <h1 className="mb-8 text-3xl font-bold">
                        <Link
                            href={route("posts.index")}
                            className="text-blue-600 hover:text-blue-700"
                        >
                            Posts
                        </Link>
                        <span className="font-medium text-blue-600"> / </span>
                        Edit
                    </h1>
                </div>
                <div className="max-w-6xl p-8 bg-white rounded shadow">
                    <form name="editForm" onSubmit={handleSubmit}>
                        <div className="flex flex-col space-y-4">
                            <div>
                                <label htmlFor="title" className="block text-gray-700">Title</label>
                                <input
                                    type="text"
                                    id="title"
                                    className="w-full px-4 py-2 border border-gray-300 rounded-md"
                                    name="title"
                                    value={data.title}
                                    onChange={(e) =>
                                        setData("title", e.target.value)
                                    }
                                />
                                {errors.title && (
                                    <span className="text-red-600 text-sm">
                                        {errors.title}
                                    </span>
                                )}
                            </div>
                            <div>
                                <label htmlFor="description" className="block text-gray-700">Description</label>
                                <textarea
                                    id="description"
                                    className="w-full px-4 py-2 border border-gray-300 rounded-md"
                                    name="description"
                                    value={data.description}
                                    onChange={(e) =>
                                        setData("description", e.target.value)
                                    }
                                />
                                {errors.description && (
                                    <span className="text-red-600 text-sm">
                                        {errors.description}
                                    </span>
                                )}
                            </div>
                        </div>
                        <div className="flex justify-between mt-6">
                            <button
                                type="submit"
                                className="px-3 py-1.5 text-white bg-blue-500 rounded hover:bg-blue-600"
                            >
                                Update
                            </button>
                            <button
                                onClick={destroy}
                                tabIndex={-1}
                                type="button"
                                className="px-3 py-1.5 text-white bg-red-500 rounded hover:bg-red-600"
                            >
                                Delete
                            </button>
                        </div>
                    </form>
                </div>
            </div>
        </AuthenticatedLayout>
    );
};

export default Edit;
inertia react index

Note: For an efficient development workflow, it’s recommended to use two terminal windows:

  1. First Terminal: Run your server (e.g., php artisan serve for Laravel).
  2. Second Terminal: Execute additional commands as needed (e.g., npm run dev or yarn dev for building assets).

Tools

Tailwind CSS Gradient Generator

Pixels to Rem (and Rem to Pixels) Converter

saim ansari
saim ansari

I'm Saim Ansari, a full-stack developer with 4+ years of hands-on experience who thrives on building web applications that leave a lasting impression. When it comes to tech, I'm particularly adept at Laravel, React, Tailwind CSS, and the Tall Stack