When dealing with large datasets in Laravel applications, client-side rendering can quickly become inefficient. In this article, I’ll demonstrate how to implement server-side DataTables processing from scratch without relying on packages like Yajra Laravel DataTables.
Why Build a Custom Solution?
While packages provide convenience, building your own implementation offers:
- Complete control over the data processing pipeline
- Better understanding of the underlying mechanics
- No dependencies on third-party packages
- Customized to your specific application needs
Project Architecture
Our implementation consists of:
- Backend Service – Handles data processing and filtering
- Controller – Processes AJAX requests
- View – Contains the DataTable HTML structure
- JavaScript – Configures and initializes the DataTable
Step 1: Create a DataTable Service
First, let’s create a dedicated service to handle server-side processing:
namespace App\Services;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Schema;
class DataTableService
{
public function processDataTable($query, Request $request, $columns): array
{
// Get the table name from the query
$tableName = $query->getModel()->getTable();
// Handle search
if ($request->has('search"https://dev.to/cammanhhoang/) && !empty($request->input('search"https://dev.to/cammanhhoang/)['value"https://dev.to/cammanhhoang/])) {
$searchValue = $request->input('search"https://dev.to/cammanhhoang/)['value"https://dev.to/cammanhhoang/];
$query->where(function ($query) use ($searchValue, $columns, $tableName) {
foreach ($columns as $column) {
// Check if the column belongs to a related table
if (str_contains($column, '."https://dev.to/cammanhhoang/)) {
// Split the column name into relation and column
[$relation, $relatedColumn] = explode('."https://dev.to/cammanhhoang/, $column);
// Add a condition on the related table
$query->orWhereHas($relation, function ($query) use ($relatedColumn, $searchValue) {
$query->where($relatedColumn, 'like"https://dev.to/cammanhhoang/, '%"https://dev.to/cammanhhoang/.$searchValue.'%"https://dev.to/cammanhhoang/);
});
} else {
// Skip columns that don't exist in the table
if (Schema::hasColumn($tableName, $column)) {
$query->orWhere($column, 'like"https://dev.to/cammanhhoang/, '%"https://dev.to/cammanhhoang/.$searchValue.'%"https://dev.to/cammanhhoang/);
}
}
}
});
}
// Handle ordering
if ($request->has('order"https://dev.to/cammanhhoang/)) {
$order = $request->input('order"https://dev.to/cammanhhoang/)[0];
$orderByColumn = $columns[$order['column"https://dev.to/cammanhhoang/]];
$orderDirection = $order['dir"https://dev.to/cammanhhoang/];
if (Schema::hasColumn($tableName, $orderByColumn)) {
$query->orderBy($orderByColumn, $orderDirection);
}
}
// Get total count before pagination
$totalFiltered = $query->count();
// Handle pagination
$start = $request->input('start"https://dev.to/cammanhhoang/, 0);
$length = $request->input('length"https://dev.to/cammanhhoang/, 10);
$query->skip($start)->take($length);
// Get filtered count
$totalData = $query->count();
// Prepare response data
return [
'draw' => intval($request->input('draw"https://dev.to/cammanhhoang/)),
'recordsTotal' => $totalData,
'recordsFiltered' => $totalFiltered,
'data' => $query->get(),
];
}
}
This service handles core functionality including:
- Searching across columns
- Relation-based searching
- Sorting data
- Implementing pagination
- Formatting the response for DataTables
Step 2: Implement the Controller
Next, create a controller method to process DataTable requests:
public function index(Request $request)
{
if ($request->ajax()) {
$columns = ['id"https://dev.to/cammanhhoang/, 'family_name"https://dev.to/cammanhhoang/, 'first_name"https://dev.to/cammanhhoang/, 'email"https://dev.to/cammanhhoang/, 'created_at"https://dev.to/cammanhhoang/, 'id"https://dev.to/cammanhhoang/];
$data = User::select($columns);
$response = $this->dataTableService->processDataTable($data, $request, $columns);
// Add URLs and format fields for each record
$response['data"https://dev.to/cammanhhoang/] = $response['data"https://dev.to/cammanhhoang/]->map(function ($user) {
$user->edit_url = route('admin.users.edit"https://dev.to/cammanhhoang/, $user->id);
$user->destroy_url = route('admin.users.destroy"https://dev.to/cammanhhoang/, $user->id);
$user->email = ' . $user->email . '">' . $user->email . '"https://dev.to/cammanhhoang/;
return $user;
});
return response()->json($response);
}
return view('admin.users.index"https://dev.to/cammanhhoang/);
}
The controller:
- Checks if the request is an AJAX call
- Sets up the columns to query
- Passes the query builder to our service
- Enhances the response with additional data (URLs, formatted content)
- Returns either JSON for AJAX requests or the view for normal requests
Step 3: Create the Blade View with Inline JavaScript
Now let’s create a complete Blade view with the DataTable initialization code included directly:
@extends('admin.layouts.master')
@section('title')
Users
@endsection
@section('page-header')
@component('admin.components.page-header')
@slot('title')
Users List
@endslot
@slot('subtitle')
Users
@endslot
@slot('button')
@endslot
@endcomponent
@endsection
@section('center-scripts')
"https://cdn.datatables.net/1.13.4/js/jquery.dataTables.min.js"https://dev.to/cammanhhoang/>
"https://cdn.datatables.net/buttons/2.3.6/js/dataTables.buttons.min.js"https://dev.to/cammanhhoang/>
@endsection
@section('content')
class="content"https://dev.to/cammanhhoang/>
class="card"https://dev.to/cammanhhoang/>
class="table datatable-selection-single" id="user-table"https://dev.to/cammanhhoang/>
ID
Last Name
First Name
Email
Created At
class="text-center"https://dev.to/cammanhhoang/>Actions
@endsection
@section('scripts')
// DataTable language configuration
let dataTableLanguage = {
"https://dev.to/cammanhhoang/loadingRecords"https://dev.to/cammanhhoang/: "https://dev.to/cammanhhoang/Loading..."https://dev.to/cammanhhoang/,
"https://dev.to/cammanhhoang/searchPlaceholder"https://dev.to/cammanhhoang/: "https://dev.to/cammanhhoang/Search..."https://dev.to/cammanhhoang/,
"https://dev.to/cammanhhoang/lengthMenu"https://dev.to/cammanhhoang/: "https://dev.to/cammanhhoang/_MENU_"https://dev.to/cammanhhoang/,
"https://dev.to/cammanhhoang/paginate"https://dev.to/cammanhhoang/: {
"https://dev.to/cammanhhoang/first"https://dev.to/cammanhhoang/: "https://dev.to/cammanhhoang/First"https://dev.to/cammanhhoang/,
"https://dev.to/cammanhhoang/last"https://dev.to/cammanhhoang/: "https://dev.to/cammanhhoang/Last"https://dev.to/cammanhhoang/,
"https://dev.to/cammanhhoang/next"https://dev.to/cammanhhoang/: "https://dev.to/cammanhhoang/→"https://dev.to/cammanhhoang/,
"https://dev.to/cammanhhoang/previous"https://dev.to/cammanhhoang/: "https://dev.to/cammanhhoang/←'
},
"https://dev.to/cammanhhoang/sLengthMenu"https://dev.to/cammanhhoang/: "https://dev.to/cammanhhoang/Display _MENU_ items"https://dev.to/cammanhhoang/,
"https://dev.to/cammanhhoang/oPaginate"https://dev.to/cammanhhoang/: {
"https://dev.to/cammanhhoang/sNext"https://dev.to/cammanhhoang/: "https://dev.to/cammanhhoang/Next"https://dev.to/cammanhhoang/,
"https://dev.to/cammanhhoang/sPrevious"https://dev.to/cammanhhoang/: "https://dev.to/cammanhhoang/Previous"
},
"https://dev.to/cammanhhoang/sInfo"https://dev.to/cammanhhoang/: "https://dev.to/cammanhhoang/Displaying _START_ to _END_ of _TOTAL_ items."https://dev.to/cammanhhoang/,
"https://dev.to/cammanhhoang/sSearch"https://dev.to/cammanhhoang/: "https://dev.to/cammanhhoang/_INPUT_"https://dev.to/cammanhhoang/,
"https://dev.to/cammanhhoang/sZeroRecords"https://dev.to/cammanhhoang/: "https://dev.to/cammanhhoang/No data to display"https://dev.to/cammanhhoang/,
"https://dev.to/cammanhhoang/sInfoEmpty"https://dev.to/cammanhhoang/: "https://dev.to/cammanhhoang/Displaying 0 of 0 items"https://dev.to/cammanhhoang/,
"https://dev.to/cammanhhoang/sInfoFiltered"https://dev.to/cammanhhoang/: "https://dev.to/cammanhhoang/Filter from all _MAX_ items"
};
$(document).ready(function () {
// Initialize DataTable directly without a helper function
$("https://dev.to/cammanhhoang/#user-table"https://dev.to/cammanhhoang/).DataTable({
retrieve: true,
processing: true,
serverSide: true,
ajax: {
url: "https://dev.to/cammanhhoang/{{ route('admin.users.index') }}"https://dev.to/cammanhhoang/,
type: "https://dev.to/cammanhhoang/GET"
},
columns: [
{"https://dev.to/cammanhhoang/data"https://dev.to/cammanhhoang/: "https://dev.to/cammanhhoang/id"https://dev.to/cammanhhoang/},
{"https://dev.to/cammanhhoang/data"https://dev.to/cammanhhoang/: "https://dev.to/cammanhhoang/family_name"https://dev.to/cammanhhoang/},
{"https://dev.to/cammanhhoang/data"https://dev.to/cammanhhoang/: "https://dev.to/cammanhhoang/first_name"https://dev.to/cammanhhoang/},
{"https://dev.to/cammanhhoang/data"https://dev.to/cammanhhoang/: "https://dev.to/cammanhhoang/email"https://dev.to/cammanhhoang/},
{"https://dev.to/cammanhhoang/data"https://dev.to/cammanhhoang/: "https://dev.to/cammanhhoang/created_at"https://dev.to/cammanhhoang/},
{
data: null,
sortable: false,
render: function (data) {
return ``;
},
},
],
dom: "https://dev.to/cammanhhoang/<"datatable-header justify-content-start"f<"ms-sm-auto"l><"ms-sm-3"B>><"datatable-scroll"t><"datatable-footer"ip>"https://dev.to/cammanhhoang/,
language: dataTableLanguage
});
// Delete confirmation handler
$(document).on("https://dev.to/cammanhhoang/click"https://dev.to/cammanhhoang/, "https://dev.to/cammanhhoang/.delete-button"https://dev.to/cammanhhoang/, function (event) {
event.preventDefault();
const form = $(this).closest("https://dev.to/cammanhhoang/form"https://dev.to/cammanhhoang/);
// Example of a simple delete confirmation
if (confirm("https://dev.to/cammanhhoang/Are you sure you want to delete this user?"https://dev.to/cammanhhoang/)) {
form.submit();
}
});
});
@endsection
Advantages of This Approach
- No External Dependencies: The implementation doesn’t rely on packages like Yajra Laravel DataTables
- Self-Contained Templates: All JavaScript is included directly in the Blade template
- Easier Debugging: Having the code in one place makes it easier to understand and debug
- Performance Optimization: Server-side processing only fetches the data needed for each request
- Customizable: You have complete control over the implementation
Error Handling
To add error handling, you can modify the AJAX call to include error callbacks:
// Inside the DataTable initialization
ajax: {
url: "{{ route('admin.users.index') }}"https://dev.to/cammanhhoang/,
type: "GET"https://dev.to/cammanhhoang/,
error: function (xhr, error, thrown) {
console.error('DataTable error:"https://dev.to/cammanhhoang/, error);
alert('An error occurred while loading data. Please try again."https://dev.to/cammanhhoang/);
}
}
Demo
Check out the working demo here:
Conclusion
Building a custom server-side DataTable implementation gives you complete control over your data processing pipeline. This approach is particularly useful for applications with specific requirements or those that need to minimize dependencies.
By merging all JavaScript code directly into the Blade template, we’ve created a self-contained solution that’s easier to maintain and debug. The backend service ensures efficient data processing, while the controller handles application-specific logic.
This solution can be extended to handle more complex scenarios like relation searches, custom filtering, or specific data formatting needs.