In this tutorial, we’ll walk through how to build a Laravel Ajax CRUD with jQuery step by step. This guide is perfect for beginners who want to learn how to perform Create, Read, Update, and Delete operations without page reloads using jQuery Ajax and Bootstrap modal forms. We’ll cover everything from creating the database table to setting up models, controllers, views, and routes — all using Laravel Ajax CRUD with jQuery. By the end of this guide, you’ll be able to build a clean and dynamic CRUD system that runs smoothly with jQuery-powered Ajax in a Laravel project.
Table of Contents
Step 1: Create Laravel Project
Open your Terminal/CMD, go to the project’s root folder, and then run the command below:
composer global require laravel/installerlaravel new laravel-ajax-crud-jquery-modalStep 2: Create Database Table
Before we start building our Laravel Ajax CRUD with jQuery system, we need to configure the database connection correctly. This ensures your application can store and retrieve data effectively.
Configure your Laravel project to connect with MySQL or other databases using the .env file — the first step for any Laravel CRUD project.

To start building our Laravel Ajax CRUD with jQuery application, we need a database table to store our records. Using Laravel migrations, we’ll define the structure of the table that will hold our CRUD data. This is the foundation for managing data dynamically with jQuery Ajax.
Open your Terminal/CMD, go to the project’s root folder, and then run the command below:
php artisan make:migration create_customers_tableUpdate the up() function with content below
// Laravel Ajax CRUD with jQuery - laravelcenter.com
public function up(): void
{
Schema::create('customers', function (Blueprint $table) {
$table->id();
$table->string('name', 50);
$table->tinyInteger('gender');
$table->string('email', 100);
$table->string('image')->nullable();
$table->timestamps();
});
}Run the migration:
php artisan migrateDatabase Error Fix: Specified key was too long
If you encounter the following error during a migration (often when running php artisan migrate):
SQLSTATE[42000]: Syntax error or access violation: 1071 Specified key was too long; max key length is 1000 bytes...
![SQLSTATE[42000] 1071 Specified key was too long](https://laravelcenter.com/wp-content/uploads/2025/06/SQLSTATE42000-1071-Specified-key-was-too-long-1024x523.webp)
This issue typically occurs when using an older version of MySQL (pre-5.7.7) or an outdated MariaDB version with Laravel’s default settings.
You need to tell the Laravel database schema builder to use a smaller default string length.
Open the file app/Providers/AppServiceProvider.php. add the following line:
use Illuminate\Support\Facades\Schema;
public function boot(): void
{
// Add this line:
Schema::defaultStringLength(191);
}After saving the file, you can try running your migrations again:
php artisan migrateStep 3: Create Model
Next, we’ll create a model that will interact with the database table. In a Laravel Ajax CRUD with jQuery setup, the model helps structure and retrieve data efficiently. It acts as a bridge between the database and your Ajax-powered frontend.
Create a file named Customer.php in the PROJECT_ROOT/app/Models directory. You can either use an artisan command or create the file manually.
<?php
// Laravel Ajax CRUD with jQuery - laravelcenter.com
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Customer extends Model
{
//
}Step 4: Create Controller with Ajax Logic
In this step, we’ll create a controller to handle CRUD logic. The controller manages data operations and responds to jQuery Ajax requests. It’s a key part of our Laravel Ajax CRUD with jQuery structure that keeps everything organized and dynamic.
Create a file named CustomerController.php in the PROJECT_ROOT/app/Http/Controllers directory. You can either use an artisan command or create the file manually.
<?php
// Laravel Ajax CRUD with jQuery - laravelcenter.com
namespace App\Http\Controllers;
use App\Models\Customer;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
class CustomerController extends Controller
{
public function index(Request $request)
{
session()->put('search', $request->get('search', session()->get('search')));
session()->put('gender', $request->get('gender', session()->get('gender', 0)));
session()->put('field', $request->get('field', session()->get('field', 'created_at')));
session()->put('order', $request->get('order', session()->get('order', 'desc')));
$customers = Customer::when(session()->get('gender') > 0, function ($query) {
$query->where('gender', session()->get('gender'));
})
->when(session()->get('search'), function ($query) {
$query->where('name', 'like', '%' . session()->get('search') . '%');
})->orderBy(session()->get('field'), session()->get('order'))
->paginate(5);
if ($request->ajax())
return view('customer.index', compact('customers'));
else
return view('customer.layout', compact('customers'));
}
public function edit($id)
{
$customer = Customer::find($id);
return view('customer.form', compact('customer'));
}
public function submit(Request $request)
{
// validation
$rules = [
'name' => 'required|unique:customers,name,' . $request->id,
'gender' => 'required',
'email' => 'required|email:rfc,dns'
];
$validator = Validator::make($request->all(), $rules);
if ($validator->fails())
return response()->json([
'success' => false,
'errors' => $validator->errors()
]);
// save to database
if ($request->id > 0) {
$data = Customer::find($request->id);
} else {
$data = new Customer();
}
$data->name = $request->name;
$data->gender = $request->gender;
$data->email = $request->email;
// delete uploaded file
if ($request->is_deleted_image == 1 && $request->id > 0) {
if (Storage::disk('public')->exists($data->image)) {
Storage::disk('public')->delete($data->image);
}
$data->image = '';
}
// upload file
else if ($request->hasFile('image')) {
if ($data->image && Storage::disk('public')->exists($data->image)) {
Storage::disk('public')->delete($data->image);
}
$data->image = Storage::disk('public')->put('customer', $request->image);
}
$data->save();
return response()->json([
'success' => true,
'redirect_url' => url('/')
]);
}
public function delete(Request $request)
{
$data = Customer::find($request->delete_id);
// delete uploaded file
if ($data->image && Storage::disk('public')->exists($data->image)) {
Storage::disk('public')->delete($data->image);
}
$data->delete();
return redirect('/');
}
}This controller ensures a smooth flow for creating, reading, updating, and deleting records while supporting image file uploads in our Laravel Modal Form Ajax CRUD with image upload project.
Step 5: Create Blade Views with Ajax Integration
In this tutorial, we’re using Bootstrap to style the CRUD interface, enhancing the appearance of elements such as buttons, forms, and icons. To get started, you’ll need to install Bootstrap and Bootstrap Icons, which Bootstrap relies on for components like dropdowns and tooltips.
Run the following command to install them via npm:
npm install jquery bootstrap bootstrap-iconsAfter installing the necessary packages—including jQuery, which we’ll use for handling Ajax requests—you need to import them into your project. Open the resources/js/app.js file and update it as follows:
import './bootstrap';
import 'bootstrap/dist/css/bootstrap.min.css';
import 'bootstrap-icons/font/bootstrap-icons.min.css';
import * as bootstrap from 'bootstrap';
import jQuery from 'jquery';
window.bootstrap = bootstrap;
window.$ = jQuery;Now we’ll create Blade view files that display the UI. These views will include a Bootstrap modal form for user interaction. Using Laravel Ajax CRUD with jQuery, the views allow users to create and update records without refreshing the page.
Create a file named layout.blade.php in the PROJECT_ROOT/resources/views/customer directory. You can either use an artisan command or create the file manually.
<!DOCTYPE html>
<html lang="{{ app()->getLocale() }}">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- CSRF Token -->
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ env('APP_NAME') }}</title>
<style>
form .required label:after {
content: " *";
color: red;
font-weight: bold;
}
.loading {
background: lightgoldenrodyellow url('processing.gif') no-repeat center 65%;
height: 80px;
width: 100px;
position: fixed;
border-radius: 4px;
left: 50%;
top: 50%;
margin: -40px 0 0 -50px;
z-index: 2000;
display: none;
}
</style>
@vite('resources/js/app.js')
</head>
<body style="display: none;">
<div class="loading"></div>
<div class="modal fade" id="form_modal" tabindex="-1" aria-hidden="false" data-bs-keyboard="false"
data-bs-backdrop="static" data-bs-focus="false">
<div class="modal-dialog modal-lg">
<div class="modal-content"></div>
</div>
</div>
<div class="fade modal" tabindex="-1" id="alert_modal" aria-hidden="false" data-bs-focus="false">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Alert</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p></p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Ok</button>
</form>
</div>
</div>
</div>
</div>
<div class="fade modal" tabindex="-1" id="confirm-delete" aria-hidden="false" data-bs-focus="false">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Confirm Delete</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>Are you sure want to delete?</p>
</div>
<div class="modal-footer">
<form id="frm_delete" action="{{ url('/delete') }}" method="post"
style="padding-bottom: 0px;margin-bottom: 0px;">
@method('DELETE')
@csrf
<input type="hidden" name="delete_id" id="delete_id" />
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-danger" data-bs-dismiss="modal">Delete</button>
</form>
</div>
</div>
</div>
</div>
<nav class="navbar navbar-expand-md navbar-dark fixed-top bg-success">
<div class="container-fluid">
<a class="navbar-brand fw-bold" href="{{ url('/') }}">Laravel Center</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarCollapse"
aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
</div>
</nav>
<main style="margin-top: 100px;" id="content">
@include('customer.index')
</main>
</body>
<script>
document.addEventListener('DOMContentLoaded', function() {
$("body").show();
const formModal = document.getElementById("form_modal");
const popupModal = new bootstrap.Modal(formModal);;
if (formModal) {
formModal.addEventListener("shown.bs.modal", (event) => {
$("#name").trigger("focus");
});
formModal.addEventListener("hide.bs.modal", (event) => {
document.activeElement?.blur();
});
}
const deleteModal = document.getElementById("confirm-delete");
if (deleteModal) {
deleteModal.addEventListener("show.bs.modal", (event) => {
var data = $(event.relatedTarget).data();
$("input#delete_id").val(data.recordId);
});
}
$(document).on("click", "a.page-link", function(event) {
event.preventDefault();
ajaxLoad($(this).attr("href"));
});
$(document).on("submit", "form#frm", function(event) {
event.preventDefault();
$(".loading").show();
var form = $(this);
var data = new FormData(form[0]);
var url = form.attr("action");
$.ajaxSetup({
headers: {
"X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr("content"),
},
});
$.ajax({
type: "POST",
url: url,
data: data,
cache: false,
contentType: false,
processData: false,
success: function(data) {
$(".is-invalid").removeClass("is-invalid");
$("span.invalid-feedback").remove();
if (!data.success) {
for (var control in data.errors) {
$("#" + control).addClass("is-invalid");
$(
"<span class='invalid-feedback'>" +
data.errors[control] +
"</span>"
).insertAfter($("#" + control));
}
} else {
ajaxLoad(data.redirect_url);
popupModal.hide();
}
$(".loading").hide();
},
error: function(xhr, textStatus, errorThrown) {
alert("Error: " + errorThrown);
},
});
return false;
});
$(document).on("submit", "form#frm_delete", function(event) {
event.preventDefault();
$(".loading").show();
var form = $(this);
var data = new FormData(form[0]);
var url = form.attr("action");
console.log(url);
$.ajaxSetup({
headers: {
"X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr("content"),
},
});
$.ajax({
type: "POST",
url: url,
data: data,
cache: false,
contentType: false,
processData: false,
success: function(data) {
$("#content").html(data);
$(".loading").hide();
},
error: function(xhr, textStatus, errorThrown) {
alert("Error: " + errorThrown);
},
});
return false;
});
window.ajaxLoad = function(filename, content) {
content = typeof content !== "undefined" ? content : "content";
$(".loading").show();
$.ajax({
type: "GET",
url: filename,
contentType: false,
success: function(data) {
$("#" + content).html(data);
$(".loading").hide();
},
error: function(xhr, status, error) {
alert(xhr.responseText);
},
});
};
window.ajaxPopup = function(filename, show_loading = false) {
if (show_loading) {
$(".loading").show();
}
$.ajax({
type: "GET",
url: filename,
contentType: false,
success: function(data) {
$("#form_modal .modal-content").html(data);
if (show_loading) {
$(".loading").hide();
}
popupModal.show();
},
error: function(xhr, status, error) {
alert(xhr.responseText);
},
});
};
window.changeProfile = () => {
$('#image').click();
};
window.validateFile = (event, maxFileSize = 1, validFileExtension = [".jpg", ".jpeg", ".bmp", ".gif",
".png",
".webp"
]) => {
var file = null;
var files = event.files;
if (files && files.length > 0) {
const alert = document.getElementById("alert_modal");
const modal = new bootstrap.Modal(alert);
var filesize = ((files[0].size / 1024) / 1024).toFixed(4); // MB
if (filesize <= maxFileSize) {
var blnValid = false;
for (var j = 0; j < validFileExtension.length; j++) {
var sCurExtension = validFileExtension[j];
if (files[0].name.substr(files[0].name.length - sCurExtension.length, sCurExtension
.length)
.toLowerCase() == sCurExtension.toLowerCase()) {
blnValid = true;
break;
}
}
files[0].name.substr(files[0].name.length - sCurExtension.length, sCurExtension.length)
var fileName = files[0].name.substr(0, files[0].name.length - sCurExtension.length)
.trim();
fileName = fileName.replace(/[^a-zA-Z0-9-_\s]/g, '');
if (fileName != files[0].name.substr(0, files[0].name.length - sCurExtension.length)) {
$('.modal-body p', alert).text(
"Invalid filename. Filename only allow alphanumeric chatacters.");
modal.show();
event.value = '';
} else if (blnValid) {
file = files[0];
} else {
$('.modal-body p', alert).text(files[0].name +
" is invalid, allowed extensions are:" +
validFileExtension.join(", "));
modal.show();
event.value = '';
}
} else {
if (maxFileSize < 1) {
$('.modal-body p', alert).text("Maximum file size is " + (maxFileSize * 1000)
.toString() + "KB.");
modal.show();
event.value = '';
} else {
$('.modal-body p', alert).text("Maximum file size is " + maxFileSize.toString() +
"MB.");
modal.show();
event.value = '';
}
}
}
return file;
};
window.removeProfile = () => {
$('#img_preview').attr('src', 'default.png');
$("#is_deleted_image").val(1);
};
$(document).on("change", "#image", function(event) {
if ($(this).val() != '') {
var selectedFile = validateFile(this, 1, [".jpg", ".jpeg", ".bmp", ".gif", ".png",
".webp"
]);
if (selectedFile) {
var reader = new FileReader();
reader.onload = function(e) {
$('#img_preview').attr('src', e.target.result);
}
reader.readAsDataURL(selectedFile);
}
}
});
$(document).on("submit", "form#search_form", function(event) {
event.preventDefault();
const form = event.target;
const formData = new FormData(form);
const queryString = new URLSearchParams(formData).toString();
const url = form.getAttribute("action") + "?" + queryString;
ajaxLoad(url);
});
});
</script>
</html>Create a file named index.blade.php in the PROJECT_ROOT/resources/views/customer directory. You can either use an artisan command or create the file manually.
<div class="container">
<div class="pagetitle">
<a type="button" class="btn btn-primary" style="float: right"
href="javascript:ajaxPopup('{{ url('/create') }}')">
<i class="bi bi-plus-circle"></i> Add New
</a>
<h2>Customers List</h2>
<hr />
</div>
<form method="get" id="search_form" action="{{ url('/') }}">
<div class="row pb-3">
<div class="col-md-10">
<div class="row justify-content-start">
<div class="col-lg-3 col-sm-6">
<label class="form-label mb-0">Name</label>
<input type="text" class="form-control" id="search" name="search"
value="{{ session('search') }}" placeholder="Search..." />
</div>
<div class="col-lg-3 col-sm-6">
<label class="form-label mb-0">Gender</label>
<select class="form-select" id="gender" name="gender">
<option value="0" {{ session('gender') == 0 ? 'selected' : '' }}>ALL</option>
<option value="2" {{ session('gender') == 2 ? 'selected' : '' }}>Male</option>
<option value="1" {{ session('gender') == 1 ? 'selected' : '' }}>Female</option>
</select>
</div>
</div>
</div>
<div class="col-md-2 align-self-end">
<button type="submit" class="btn btn-secondary pt-1" style="float: right">
<i class="bi bi-search"></i> Search
</button>
</div>
</div>
</form>
<table class="table table-striped">
<thead class="table-dark">
<tr>
<th width="60px" class="text-center">No</th>
<th width="150px" class="text-center">Photo</th>
<th style="cursor: pointer"
onclick="ajaxLoad('{{ url('/?field=name&order=' . (session('order') == 'asc' ? 'desc' : 'asc')) }}')">
Name
<i
class="text-secondary {{ session('field') == 'name'
? (session('order') == 'desc'
? 'bi bi-sort-alpha-down-alt'
: 'bi bi-sort-alpha-down')
: 'bi bi-arrow-down-up' }}"></i>
</th>
<th style="cursor: pointer"
onclick="ajaxLoad('{{ url('/?field=gender&order=' . (session('order') == 'asc' ? 'desc' : 'asc')) }}')">
Gender
<i
class="text-secondary {{ session('field') == 'gender'
? (session('order') == 'desc'
? 'bi bi-sort-alpha-down-alt'
: 'bi bi-sort-alpha-down')
: 'bi bi-arrow-down-up' }}"></i>
</th>
<th style="cursor: pointer"
onclick="ajaxLoad('{{ url('/?field=email&order=' . (session('order') == 'asc' ? 'desc' : 'asc')) }}')">
Email
<i
class="text-secondary {{ session('field') == 'email'
? (session('order') == 'desc'
? 'bi bi-sort-alpha-down-alt'
: 'bi bi-sort-alpha-down')
: 'bi bi-arrow-down-up' }}"></i>
</th>
<th style="cursor: pointer"
onclick="ajaxLoad('{{ url('/?field=created_at&order=' . (session('order') == 'asc' ? 'desc' : 'asc')) }}')">
Created at
<i
class="text-secondary {{ session('field') == 'created_at'
? (session('order') == 'desc'
? 'bi bi-sort-alpha-down-alt'
: 'bi bi-sort-alpha-down')
: 'bi bi-arrow-down-up' }}"></i>
</th>
<th width="150px" class="text-center">Action</th>
</tr>
</thead>
<tbody>
@foreach ($customers as $index => $value)
<tr>
<th style="vertical-align: middle;text-align: center">
{{ $customers->perPage() * ($customers->currentPage() - 1) + ($index + 1) }}
</th>
<td class="text-center py-2"><img style="width: 60px;height: 60px;"
src="{{ url($value->image ? './storage/' . $value->image : './default.png') }}" />
</td>
<td style="vertical-align: middle">{{ $value->name }}</td>
<td style="vertical-align: middle">{{ $value->gender == 2 ? 'Male' : 'Female' }}</td>
<td style="vertical-align: middle">{{ $value->email }}</td>
<td style="vertical-align: middle">{{ date('d/m/Y H:i:s', strtotime($value->created_at)) }}</td>
<td style="vertical-align: middle;text-align: center;">
<i class="bi bi-trash3-fill text-danger" role="button" data-record-id="{{ $value->id }}"
title="Delete" data-bs-toggle="modal" data-bs-target="#confirm-delete"></i>
<a title="Edit"
href="javascript:ajaxPopup('{{ url('/edit/' . $value->id) }}',true)">
<i class="bi bi-pencil-square text-success ps-3" role="button"></i>
</a>
</td>
</tr>
@endforeach
</tbody>
</table>
<nav>
<ul class="pagination justify-content-end">
{{ $customers->links() }}
</ul>
</nav>
</div>Create a file named form.blade.php in the PROJECT_ROOT/resources/views/customer directory. You can either use an artisan command or create the file manually.
<div class="modal-header py-2 bg-secondary text-light">
<h5 class="modal-title" style="font-weight: bold">
{{ isset($customer) ? 'Edit' : 'New' }} Customer
</h5>
</div>
<div class="modal-body">
<form method="POST" id="frm" enctype="multipart/form-data" action="{{ url('/submit') }}">
@csrf
@method(isset($customer) ? 'PUT' : 'POST')
<input type="hidden" value="{{ isset($customer) ? $customer->id : 0 }}" name="id" />
<div class="row">
<div class="col-md-6">
<div class="required mb-3">
<label for="name" class="form-label">Name</label>
<input id="name" name="name" type="text" class="form-control"
value="{{ isset($customer) ? $customer->name : '' }}" />
</div>
<div class="required mb-3">
<label for="gender" class="form-label">Gender</label>
<select id="gender" name="gender" class="form-select">
<option value="2" @if (isset($customer) ? $customer->gender : '' == 'Male') selected @endif>Male
</option>
<option value="1" @if (isset($customer) ? $customer->gender : '' == 'Female') selected @endif>Female
</option>
</select>
</div>
<div class="required mb-3">
<label for="email" class="form-label">Email</label>
<input id="email" name="email" type="text"
value="{{ isset($customer) ? $customer->email : '' }}" class="form-control">
</div>
</div>
<div class="col-md-6">
<div class="mb-3 text-center">
<label class="form-label">Photo</label>
<div>
<input type="hidden" style="display: none" value="0" name="is_deleted_image"
id="is_deleted_image">
<img id="img_preview" style="width: 180px;height: 180px;cursor: pointer;"
class="img img-thumbnail" onerror="this.onerror=null;this.src='./default.png'"
src="{{ isset($customer) && $customer->image ? url('./storage/' . $customer->image) : url('./default.png') }}"
onclick="changeProfile()" />
<p>
<a href="javascript:changeProfile()" style="text-decoration: none;">
Change</a>
<a href="javascript:removeProfile()" style="color: red;text-decoration: none;">Remove</a>
</p>
<input type="file" id="image" name="image" style="display: none;"
accept=".jpg,.jpeg,.bmp,.gif,.png,.webp" />
</div>
</div>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger" data-bs-dismiss="modal">
<i class="bi bi-x-lg"></i> Cancel
</button>
<button type="submit" class="btn btn-primary" form="frm">
<i class="bi bi-floppy" style="padding-right: 3px;"></i>Save</button>
</div>Step 6: Define Routes (Standard and Ajax)
Finally, we’ll define routes to connect everything together. These routes will handle Ajax requests sent by jQuery and point them to the appropriate controller actions. Defining proper routes is crucial for smooth operation of Laravel Ajax CRUD with jQuery.
In routes/web.php:
<?php
// Laravel Ajax CRUD with jQuery - laravelcenter.com
use App\Http\Controllers\CustomerController;
use Illuminate\Support\Facades\Route;
Route::controller(CustomerController::class)->group(function () {
Route::get('/', 'index');
Route::view('create', 'customer.form');
Route::get('edit/{id}', 'edit');
Route::match(['post', 'put'], 'submit', 'submit');
Route::delete('delete', 'delete');
});Additional Setup
💡 Tip: To ensure the project loads smoothly without broken images, you can download this default image and place it inside your Laravel project’s
public/folder. This helps prevent errors when no uploaded images are available yet.
Fixing Pagination Styling with Bootstrap in Laravel
By default, Laravel uses Tailwind CSS for pagination views. If your project is using Bootstrap (especially Bootstrap 4 or 5), the pagination links generated by {{ $items->links() }} may look unstyled or broken.
To fix this, you need to tell Laravel to use Bootstrap-compatible pagination views. You can do this by updating the AppServiceProvider.
Open the file:app/Providers/AppServiceProvider.php
Inside the boot() method, add the following line:
use Illuminate\Pagination\Paginator;
public function boot()
{
Paginator::useBootstrap();
}This tells Laravel to render pagination links using Bootstrap’s markup instead of Tailwind CSS.
Set Up Laravel Storage Symlink
When you’re handling image uploads in Laravel, the uploaded files are typically stored in the storage/app/public directory. However, these files need to be publicly accessible from the browser. Laravel provides a convenient command to create a symbolic link between the public/storage directory and storage/app/public:
php artisan storage:linkThis command will generate a symbolic link so you can access uploaded images using a public URL like /storage/your-image.jpg.
Running and Testing Your Project
Open your Terminal/CMD in separate windows, go to the project’s root folder, and then run the command below:
npm run devphp artisan serveWith both commands running in their separate windows, open your web browser to the Laravel address (http://127.0.0.1:8000). You should now see the customer list with image upload, search, sort, and pagination features working as expected.



By following this guide, you now have a solid understanding of building a full-featured Laravel Ajax CRUD with jQuery, search, sort, and pagination.
Conclusion
In this guide, you’ve learned how to build a complete Laravel Ajax CRUD with jQuery application from start to finish. By integrating Laravel’s powerful backend with jQuery and Ajax, you can perform create, read, update, and delete operations instantly — without reloading the page.
This Laravel Ajax CRUD with jQuery setup improves performance, simplifies data handling, and provides a seamless user experience. You can extend it further by adding image uploads, real-time validation, or pagination for a more dynamic system.
Mastering Laravel Ajax CRUD with jQuery is a great step toward building fast, interactive, and professional web applications.







