Luke Anderson Laravel

Laravel + React.js Project Tutorial – Build a Full CRUD Employee Manager Step-by-Step

If you’ve used Laravel for backend APIs and React for frontends separately, you’ve probably wondered: can I just put them together in the same project and skip the CORS, the build setup, and the deployment hassle?

Yes. Laravel ships with first-class React support. In this tutorial you’ll build a complete full-stack Employee Manager CRUD app with Laravel as the backend (database, models, API) and React as the frontend (table, modals, forms). One repo. One deployment. Real authentication. Working create / view / update / delete on real data.

By the end you’ll have a production-shaped starter you can fork into any internal tool — customer manager, inventory system, ticket tracker, anything that’s basically “a table with CRUD.”

📺 Watch the full video walkthrough (2h 28m): https://youtu.be/svziC8BblM0

The video shows every keystroke, including the React class-component patterns, modal wiring, and database seeding with Faker. This post is the written companion you can come back to.

What You’ll Build

A full-stack web app called EmployeeManager with:

  • ✅ Laravel 8/9 backend with MySQL
  • ✅ Laravel UI + React scaffold (one project, not two)
  • ✅ Laravel’s built-in auth (login, register, password reset)
  • ✅ employees table seeded with 100 fake employees via Factory + Faker
  • ✅ RESTful API endpoints: list, get one, create, update, delete
  • ✅ React frontend with Bootstrap 5 table
  • ✅ Modal-based UI: View, Create, Update, Delete
  • ✅ React class components with state, props, getDerivedStateFromProps
  • ✅ Axios for AJAX between React and Laravel
  • ✅ Error handling with try/catch + Laravel logging

Prerequisites

Before starting, you need:

Tool Why Where to get it
PHP 8.0+ Laravel runtime XAMPP / php.net
Composer PHP dependency manager getcomposer.org
Node.js 14+ & npm React build tooling nodejs.org
MySQL or MariaDB Database XAMPP includes both
A code editor VS Code or PhpStorm code.visualstudio.com
Basic PHP + JS knowledge You don’t need to be an expert —

💡 Easiest local setup: XAMPP gives you Apache + MySQL + PHP in one installer. For Docker-based setups, see our WSL + Docker + PHP guide.

Step 1: Create the Laravel Project

Open your terminal in the folder where you keep projects:

composer create-project laravel/laravel EmployeeManager
cd EmployeeManager

Test it works:

php artisan serve

Open http://127.0.0.1:8000 — you should see Laravel’s welcome page. Press Ctrl+C to stop the server for now.

Step 2: Configure the Database

Create a new database in MySQL (via phpMyAdmin or CLI):

CREATE DATABASE employee_manager;

Open .env in your project root and update the database section:

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=employee_manager
DB_USERNAME=root
DB_PASSWORD=

Run the default migrations (creates users, password_resets, etc.):

php artisan migrate

If you see a “Specified key was too long” error, edit app/Providers/AppServiceProvider.php and add inside the boot() method:

use Illuminate\Support\Facades\Schema;

public function boot()
{
    Schema::defaultStringLength(191);
}

Then re-run: php artisan migrate:fresh.

Step 3: Install Laravel UI with React

Laravel doesn’t ship React by default — you install it via the laravel/ui package, which scaffolds React + auth views in one command.

composer require laravel/ui
php artisan ui react --auth
npm install
npm run dev

What just happened:

  • laravel/ui package installed
  • React scaffold generated in resources/js/
  • Auth views generated in resources/views/auth/
  • Webpack/Vite builds your JS into public/js/app.js

You’ll now find:

resources/
├── js/
│   ├── components/
│   │   └── Example.js     ← the demo React component
│   ├── app.js
│   ├── bootstrap.js
│   └── index.js
└── views/
    ├── auth/
    │   ├── login.blade.php
    │   ├── register.blade.php
    │   └── ...
    └── home.blade.php

Visit http://127.0.0.1:8000/register — you’ll see the auth registration form already working. Register a test account.

⚠️ npm run dev vs npm run watch — Use npm run watch during development; it automatically rebuilds when you save a .js or .jsx file. npm run dev is a one-time build.

Step 4: Mount React Inside the Home View

Open resources/views/home.blade.php. You’ll see Laravel’s default authenticated home page. Replace the content with a mount point for React:

@extends('layouts.app')

@section('content')
<div class="container" id="employeeApp">
    <!-- React will render here -->
</div>
@endsection

Now update resources/js/app.js to render the example component into this div. Open resources/js/components/Example.js and you’ll see the default example. Update its mounting line at the bottom:

if (document.getElementById('employeeApp')) {
    ReactDOM.render(<Example />, document.getElementById('employeeApp'));
}

Run npm run watch in a separate terminal. Reload /home — you should see “I’m an example component!” rendered by React inside a Laravel page.

🎉 You now have Laravel + React talking to each other. Everything from here is just building out the CRUD.

Step 5: Create the Employee Model and Migration

Generate the model, migration, factory, and seeder all in one command:

php artisan make:model Employee -mfs

The -mfs flags create migration, factory, and seeder. Now edit:

database/migrations/xxxx_create_employees_table.php

public function up()
{
    Schema::create('employees', function (Blueprint $table) {
        $table->id();
        $table->string('employee_name');
        $table->integer('salary');
        $table->timestamps();
    });
}

app/Models/Employee.php

<?php

namespace App\Models;

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

class Employee extends Model
{
    use HasFactory;

    protected $fillable = [
        'employee_name',
        'salary',
    ];
}

🔑 Why $fillable matters: Laravel’s mass assignment protection blocks Employee::create($data) unless you whitelist the fields. Without this, your form submissions silently fail.

database/factories/EmployeeFactory.php

public function definition()
{
    return [
        'employee_name' => $this->faker->name(),
        'salary' => $this->faker->numberBetween(50000, 400000),
    ];
}

database/seeders/EmployeeSeeder.php

public function run()
{
    \App\Models\Employee::factory()->count(100)->create();
}

database/seeders/DatabaseSeeder.php — call the seeder

public function run()
{
    $this->call([
        EmployeeSeeder::class,
    ]);
}

Run the migration and seed:

php artisan migrate
php artisan db:seed

Open phpMyAdmin → employees table → you should see 100 fake employees with names and salaries. 🎉

Step 6: Create the EmployeesController

Generate the controller:

php artisan make:controller EmployeesController

Open app/Http/Controllers/EmployeesController.php and add the CRUD methods:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Employee;
use Log;
use Exception;

class EmployeesController extends Controller
{
    /
     * Get Employee List from database.
     */
    public function getEmployeeList()
    {
        try {
            $employees = Employee::orderBy('id', 'desc')->get();
            return response()->json($employees);
        } catch (Exception $e) {
            Log::error($e);
            return response()->json(['error' => 'Failed to fetch employees'], 500);
        }
    }

    /
     * Get individual employee details.
     */
    public function getEmployeeDetails(Request $request)
    {
        try {
            $employee = Employee::findOrFail($request->id);
            return response()->json($employee);
        } catch (Exception $e) {
            Log::error($e);
            return response()->json(['error' => 'Employee not found'], 404);
        }
    }

    /
     * Create a new employee.
     */
    public function createEmployeeData(Request $request)
    {
        try {
            $validated = $request->validate([
                'employee_name' => 'required|string|max:255',
                'salary' => 'required|integer|min:0',
            ]);

            $employee = Employee::create($validated);
            return response()->json($employee, 201);
        } catch (Exception $e) {
            Log::error($e);
            return response()->json(['error' => 'Failed to create employee'], 500);
        }
    }

    /
     * Update existing employee.
     */
    public function updateEmployeeData(Request $request)
    {
        try {
            $employee = Employee::findOrFail($request->id);
            $employee->update([
                'employee_name' => $request->employee_name,
                'salary' => $request->salary,
            ]);
            return response()->json($employee);
        } catch (Exception $e) {
            Log::error($e);
            return response()->json(['error' => 'Failed to update employee'], 500);
        }
    }

    /
     * Delete employee.
     */
    public function deleteEmployeeData(Request $request)
    {
        try {
            Employee::destroy($request->id);
            return response()->json(['message' => 'Employee deleted']);
        } catch (Exception $e) {
            Log::error($e);
            return response()->json(['error' => 'Failed to delete'], 500);
        }
    }
}

💡 Why try/catch on every method? API endpoints should never crash with a stack trace exposed to the user. Log::error($e) writes to storage/logs/laravel.log so you can debug later without exposing internals.

Step 7: Define the Routes

Open routes/web.php and add the API routes:

<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\EmployeesController;

Route::get('/', function () {
    return view('welcome');
});

Auth::routes();

Route::get('/home', [App\Http\Controllers\HomeController::class, 'index'])->name('home');

// Employee API endpoints
Route::get('/get/employee/list',
    [EmployeesController::class, 'getEmployeeList'])->name('employee.list');

Route::post('/get/individual/employee/details',
    [EmployeesController::class, 'getEmployeeDetails'])->name('employee.details');

Route::post('/create/employee/data',
    [EmployeesController::class, 'createEmployeeData']);

Route::post('/update/employee/data',
    [EmployeesController::class, 'updateEmployeeData']);

Route::delete('/delete/employee/data',
    [EmployeesController::class, 'deleteEmployeeData']);

Test in a browser: http://127.0.0.1:8000/get/employee/list should return JSON with 100 employees.

⚠️ Routes in web.php vs api.php: We’re using web.php because we want CSRF protection and access to the authenticated session. If you put these in api.php you’d need API tokens (Sanctum) instead.

Step 8: Build the React Component Structure

Create a folder structure for organized components:

resources/js/components/
├── employeeList/
│   ├── Table.js
│   ├── TableRow.js
│   ├── TableActionButtons.js
│   └── Modals/
│       ├── CreateModal.js
│       ├── ViewModal.js
│       ├── UpdateModal.js
│       └── DeleteModal.js
└── App.js

resources/js/components/App.js

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import Table from './employeeList/Table';
import CreateModal from './employeeList/Modals/CreateModal';

class App extends Component {
    render() {
        return (
            <div>
                <div className="text-end my-4">
                    <button
                        type="button"
                        className="btn btn-info btn-lg"
                        data-bs-toggle="modal"
                        data-bs-target="#createModal"
                    >
                        Add New Employee
                    </button>
                </div>

                <Table />
                <CreateModal />
            </div>
        );
    }
}

export default App;

if (document.getElementById('employeeApp')) {
    ReactDOM.render(<App />, document.getElementById('employeeApp'));
}

resources/js/components/employeeList/Table.js

import React, { Component } from 'react';
import TableRow from './TableRow';

class Table extends Component {
    constructor(props) {
        super(props);

        this.state = {
            employees: [],
        };
    }

    componentDidMount() {
        this.getEmployeeList();
    }

    // Get Employee List from Laravel API
    getEmployeeList = () => {
        axios.get('/get/employee/list')
            .then(response => {
                this.setState({ employees: response.data });
            })
            .catch(error => {
                console.error('Error fetching employees:', error);
            });
    }

    render() {
        return (
            <div className="container">
                <div className="row justify-content-center">
                    <div className="col-md-8">
                        <div className="card">
                            <table className="table table-hover">
                                <thead>
                                    <tr>
                                        <th scope="col" width="100px">#</th>
                                        <th scope="col" width="100px">Name</th>
                                        <th scope="col" width="100px">Salary</th>
                                        <th scope="col" width="100px">Actions</th>
                                    </tr>
                                </thead>
                                <tbody>
                                    {this.state.employees.map(function (x, i) {
                                        return <TableRow key={i} data={x} />
                                    })}
                                </tbody>
                            </table>
                        </div>
                    </div>
                </div>
            </div>
        );
    }
}

export default Table;

resources/js/components/employeeList/TableRow.js

import React, { Component } from 'react';
import TableActionButtons from './TableActionButtons';

class TableRow extends Component {
    render() {
        return (
            <tr>
                <td>{this.props.data.id}</td>
                <td>{this.props.data.employee_name}</td>
                <td>{this.props.data.salary}</td>
                <td>
                    <TableActionButtons employeeId={this.props.data.id} />
                </td>
            </tr>
        );
    }
}

export default TableRow;

resources/js/components/employeeList/TableActionButtons.js

import React, { Component } from 'react';

class TableActionButtons extends Component {
    constructor(props) {
        super(props);
    }

    render() {
        return (
            <div className="btn-group" role="group">
                <button
                    type="button"
                    className="btn btn-primary"
                    data-bs-toggle="modal"
                    data-bs-target={`#viewModal-${this.props.employeeId}`}
                >
                    View
                </button>
                <button
                    type="button"
                    className="btn btn-info"
                    data-bs-toggle="modal"
                    data-bs-target={`#updateModal-${this.props.employeeId}`}
                >
                    Update
                </button>
                <button
                    type="button"
                    className="btn btn-danger"
                    data-bs-toggle="modal"
                    data-bs-target={`#deleteModal-${this.props.employeeId}`}
                >
                    Delete
                </button>
            </div>
        );
    }
}

export default TableActionButtons;

Step 9: Build the Create Modal

resources/js/components/employeeList/Modals/CreateModal.js

import React, { Component } from 'react';

class CreateModal extends Component {
    constructor(props) {
        super(props);

        this.state = {
            employeeName: '',
            employeeSalary: '',
        };
    }

    inputEmployeeName = (event) => {
        this.setState({ employeeName: event.target.value });
    }

    inputEmployeeSalary = (event) => {
        this.setState({ employeeSalary: event.target.value });
    }

    createEmployeeData = () => {
        axios.post('/create/employee/data', {
            employee_name: this.state.employeeName,
            salary: this.state.employeeSalary,
        })
        .then(response => {
            window.location.reload();
        })
        .catch(error => {
            console.error('Create failed:', error);
        });
    }

    render() {
        return (
            <div className="modal fade" id="createModal" tabIndex="-1">
                <div className="modal-dialog">
                    <div className="modal-content">
                        <div className="modal-header">
                            <h5 className="modal-title">Add New Employee</h5>
                            <button
                                type="button"
                                className="btn-close"
                                data-bs-dismiss="modal"
                                aria-label="Close"
                            ></button>
                        </div>
                        <div className="modal-body">
                            <form className="form">
                                <div className="form-group mb-3">
                                    <label>Employee Name</label>
                                    <input
                                        type="text"
                                        className="form-control"
                                        id="employeeName"
                                        onChange={this.inputEmployeeName}
                                    />
                                </div>

                                <div className="form-group mb-3">
                                    <label>Salary</label>
                                    <input
                                        type="text"
                                        className="form-control"
                                        id="employeeSalary"
                                        value={this.state.employeeSalary ?? ""}
                                        onChange={this.inputEmployeeSalary}
                                    />
                                </div>
                            </form>
                        </div>
                        <div className="modal-footer">
                            <button
                                type="button"
                                className="btn btn-secondary"
                                data-bs-dismiss="modal"
                            >Close</button>
                            <button
                                type="button"
                                className="btn btn-primary"
                                onClick={this.createEmployeeData}
                            >Save Employee</button>
                        </div>
                    </div>
                </div>
            </div>
        );
    }
}

export default CreateModal;

Step 10: Build the Update Modal

This one uses getDerivedStateFromProps to sync prop changes into state — a key React pattern.

resources/js/components/employeeList/Modals/UpdateModal.js

import React, { Component } from 'react';

class UpdateModal extends Component {
    constructor(props) {
        super(props);

        this.state = {
            employeeName: null,
            employeeSalary: null,
        };
    }

    inputEmployeeName = (event) => {
        this.setState({
            employeeName: event.target.value,
        });
    }

    // Update employee salary state.
    inputEmployeeSalary = (event) => {
        this.setState({
            employeeSalary: event.target.value,
        });
    }

    static getDerivedStateFromProps(props, current_state) {
        let employeeUpdate = {
            employeeName: null,
            employeeSalary: null,
        };

        if (current_state.employeeName === null && props.data) {
            employeeUpdate.employeeName = props.data.employee_name;
            employeeUpdate.employeeSalary = props.data.salary;
            return employeeUpdate;
        }

        return null;
    }

    updateEmployeeData = () => {
        axios.post('/update/employee/data', {
            id: this.props.data.id,
            employee_name: this.state.employeeName,
            salary: this.state.employeeSalary,
        })
        .then(response => {
            window.location.reload();
        })
        .catch(error => {
            console.error('Update failed:', error);
        });
    }

    render() {
        return (
            <div className="modal fade" id={`updateModal-${this.props.data.id}`} tabIndex="-1">
                <div className="modal-dialog">
                    <div className="modal-content">
                        <div className="modal-header">
                            <h5 className="modal-title">Update Employee</h5>
                            <button type="button" className="btn-close" data-bs-dismiss="modal"></button>
                        </div>
                        <div className="modal-body">
                            <form>
                                <div className="form-group mb-3">
                                    <label>Name</label>
                                    <input
                                        type="text"
                                        className="form-control"
                                        value={this.state.employeeName ?? ""}
                                        onChange={this.inputEmployeeName}
                                    />
                                </div>
                                <div className="form-group mb-3">
                                    <label>Salary</label>
                                    <input
                                        type="text"
                                        className="form-control"
                                        value={this.state.employeeSalary ?? ""}
                                        onChange={this.inputEmployeeSalary}
                                    />
                                </div>
                            </form>
                        </div>
                        <div className="modal-footer">
                            <button type="button" className="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
                            <button type="button" className="btn btn-primary" onClick={this.updateEmployeeData}>
                                Update
                            </button>
                        </div>
                    </div>
                </div>
            </div>
        );
    }
}

export default UpdateModal;

🔑 Why getDerivedStateFromProps? When parent component re-renders with new props (e.g., a different employee), this lifecycle method syncs those props into local state. Without it, the modal would always show stale data.

Step 11: Build the Delete Modal

resources/js/components/employeeList/Modals/DeleteModal.js

import React, { Component } from 'react';

class DeleteModal extends Component {
    deleteEmployeeData = () => {
        axios.delete('/delete/employee/data', {
            data: { id: this.props.data.id }
        })
        .then(response => {
            window.location.reload();
        })
        .catch(error => {
            console.error('Delete failed:', error);
        });
    }

    render() {
        return (
            <div className="modal fade" id={`deleteModal-${this.props.data.id}`} tabIndex="-1">
                <div className="modal-dialog">
                    <div className="modal-content">
                        <div className="modal-header">
                            <h5 className="modal-title">Confirm Delete</h5>
                            <button type="button" className="btn-close" data-bs-dismiss="modal"></button>
                        </div>
                        <div className="modal-body">
                            Are you sure you want to delete <strong>{this.props.data.employee_name}</strong>?
                        </div>
                        <div className="modal-footer">
                            <button type="button" className="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
                            <button type="button" className="btn btn-danger" onClick={this.deleteEmployeeData}>
                                Delete
                            </button>
                        </div>
                    </div>
                </div>
            </div>
        );
    }
}

export default DeleteModal;

Step 12: Build the View Modal

resources/js/components/employeeList/Modals/ViewModal.js

import React, { Component } from 'react';

class ViewModal extends Component {
    render() {
        return (
            <div className="modal fade" id={`viewModal-${this.props.data.id}`} tabIndex="-1">
                <div className="modal-dialog">
                    <div className="modal-content">
                        <div className="modal-header">
                            <h5 className="modal-title">Employee Details</h5>
                            <button type="button" className="btn-close" data-bs-dismiss="modal"></button>
                        </div>
                        <div className="modal-body">
                            <p><strong>ID:</strong> {this.props.data.id}</p>
                            <p><strong>Name:</strong> {this.props.data.employee_name}</p>
                            <p><strong>Salary:</strong> ${this.props.data.salary}</p>
                        </div>
                        <div className="modal-footer">
                            <button type="button" className="btn btn-secondary" data-bs-dismiss="modal">Close</button>
                        </div>
                    </div>
                </div>
            </div>
        );
    }
}

export default ViewModal;

Step 13: Wire All Modals into TableRow

Update TableRow.js to render all the per-row modals:

import React, { Component } from 'react';
import TableActionButtons from './TableActionButtons';
import ViewModal from './Modals/ViewModal';
import UpdateModal from './Modals/UpdateModal';
import DeleteModal from './Modals/DeleteModal';

class TableRow extends Component {
    render() {
        return (
            <>
                <tr>
                    <td>{this.props.data.id}</td>
                    <td>{this.props.data.employee_name}</td>
                    <td>{this.props.data.salary}</td>
                    <td>
                        <TableActionButtons employeeId={this.props.data.id} />
                    </td>
                </tr>

                <ViewModal data={this.props.data} />
                <UpdateModal data={this.props.data} />
                <DeleteModal data={this.props.data} />
            </>
        );
    }
}

export default TableRow;

Step 14: Configure Axios for CSRF

Laravel requires a CSRF token on POST/PUT/DELETE requests. The Laravel UI scaffold already includes this in resources/js/bootstrap.js:

window.axios = require('axios');
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';

let token = document.head.querySelector('meta[name="csrf-token"]');
if (token) {
    window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content;
}

Make sure resources/views/layouts/app.blade.php has the CSRF meta tag in <head>:

<meta name="csrf-token" content="{{ csrf_token() }}">

Step 15: Build & Run

# Terminal 1: Laravel server
php artisan serve

# Terminal 2: React watcher
npm run watch

Open http://127.0.0.1:8000 → register → log in → you’ll land on /home showing the employee table with View / Update / Delete buttons working live.

🎉 Full-stack CRUD complete.

Project Structure Recap

EmployeeManager/
├── app/
│   ├── Http/Controllers/
│   │   ├── HomeController.php
│   │   └── EmployeesController.php       ← API endpoints
│   └── Models/
│       └── Employee.php                   ← Eloquent model
├── database/
│   ├── factories/EmployeeFactory.php      ← Fake data
│   ├── migrations/..._create_employees_table.php
│   └── seeders/
│       ├── DatabaseSeeder.php
│       └── EmployeeSeeder.php
├── resources/
│   ├── js/
│   │   ├── components/
│   │   │   ├── App.js                     ← Root React component
│   │   │   └── employeeList/
│   │   │       ├── Table.js
│   │   │       ├── TableRow.js
│   │   │       ├── TableActionButtons.js
│   │   │       └── Modals/
│   │   │           ├── CreateModal.js
│   │   │           ├── ViewModal.js
│   │   │           ├── UpdateModal.js
│   │   │           └── DeleteModal.js
│   │   ├── app.js
│   │   └── bootstrap.js                   ← Axios + CSRF setup
│   └── views/
│       └── home.blade.php                  ← React mount point
└── routes/
    └── web.php                             ← All routes

Common Errors and Fixes

Error Cause Fix
419 Page Expired on POST Missing CSRF token Add <meta name="csrf-token"> to layout
MethodNotAllowedHttpException Route method mismatch (GET vs POST) Check routes/web.php HTTP verb
React component not rendering Mount point ID typo Match getElementById('employeeApp') to your Blade <div id>
SQLSTATE[42000]: Specified key was too long Old MySQL default charset Add Schema::defaultStringLength(191); in AppServiceProvider
Class 'Employee' not found Missing use App\Models\Employee; Add the use statement at top of controller
npm run watch not rebuilding Webpack cache Delete node_modules/.cache and re-run
Mass assignment exception Field not in $fillable Add field to $fillable array in model
Axios returns 500 with no detail Production error mode Set APP_DEBUG=true in .env (dev only!)
Modal won’t close on submit Missing data-bs-dismiss="modal" Add to your close/submit button
Failed to resolve module specifier "axios" Axios not in window scope bootstrap.js should do window.axios = require('axios');
Old data showing in update modal Stale state Use getDerivedStateFromProps to sync props → state

Useful Artisan Commands Cheat Sheet

Command What it does
php artisan serve Start dev server at 127.0.0.1:8000
php artisan make:model Employee -mfs Model + migration + factory + seeder
php artisan make:controller EmployeesController New controller
php artisan make:migration create_employees_table New migration
php artisan migrate Run pending migrations
php artisan migrate:fresh Drop all tables + re-run migrations
php artisan migrate:fresh --seed Fresh + seed
php artisan db:seed Run seeders
php artisan route:list List all registered routes
php artisan tinker Interactive REPL for testing models
php artisan cache:clear Clear app cache
php artisan config:clear Reload .env changes
npm run watch Auto-rebuild React on save
npm run dev One-time React build
npm run production Minified production build

Pro Tips for Production

  1. Validate everything — Use Laravel’s $request->validate() in every controller method
  2. Use Form Requests — Move validation to dedicated php artisan make:request classes for complex forms
  3. Don’t reload the page after CRUD — Use React state to update the UI for instant feedback
  4. Switch to functional components + hooks — useState, useEffect are simpler than class components for new code
  5. Add Sanctum for API authentication — Required if you want a mobile app or SPA frontend
  6. Use environment configs — Different .env files for dev/staging/prod
  7. Add tests — php artisan test runs PHPUnit; React has Jest
  8. Enable Laravel Telescope in dev — Best debugging tool for Laravel apps
  9. Use migrations for ALL schema changes — Never edit DB directly in production
  10. Set APP_DEBUG=false in production — Or you’ll leak stack traces

Wrapping Up

You now have a complete full-stack Laravel + React app with:

  • ✅ Authentication out of the box
  • ✅ Database with migrations, factories, seeders
  • ✅ REST API with try/catch error handling
  • ✅ React frontend with class components and lifecycle methods
  • ✅ Bootstrap-styled modals for full CRUD
  • ✅ Axios + CSRF properly configured

This pattern scales: swap “Employee” for “Product”, “Customer”, “Ticket”, “Invoice” — anything that fits the “table with rows you can manage” mold. The same architecture powers thousands of internal tools and SaaS dashboards.

📺 Reminder: The full 2h 28m video walkthrough is at https://youtu.be/svziC8BblM0 — it covers every keystroke including the modal dialog wiring, the React getDerivedStateFromProps pattern, and the database seeder setup.

Recommended next steps

  • Add server-side pagination — for >1000 employees, paginate with Employee::paginate(20)
  • Add search and filtering — by name, salary range, department
  • Replace class components with hooks — modernize to useState, useEffect, useContext
  • Add role-based permissions — spatie/laravel-permission is the standard
  • Add file uploads — employee profile photos
  • Convert to a SPA with React Router — multi-page React inside one Laravel project
  • Add Laravel Sanctum — if you want to consume the same API from a mobile app
  • Deploy to production — DigitalOcean, AWS, Laravel Forge, or Vercel

Related reading on this blog: [csv-import-laravel-tutorial], wsl-docker-php-setup, [useful-docker-commands], [important-linux-commands-beginners]

Discussion

Be the first to comment

Leave a comment

Get a quote