From Project-Based Datatable to Laravel Composer Package
How I turned my Laravel Inertia Datatable project implementation into a reusable Composer package for server-side search, filters, sorting, relations, and pagination.

From Project-Based Datatable to Laravel Composer Package
I created a standalone Laravel Composer package called raprmdn/laravel-inertia-datatables.
The original version was built as a complete Laravel, Inertia, React, Tailwind, and shadcn/ui project. It worked as a full implementation, but the backend datatable logic was reusable enough that it should not stay locked inside one project.
So I extracted the core logic into a Composer package. The package has since reached v0.6.0, with a safer column definition API and broader support for Eloquent and Query Builder use cases.
Package: Packagist
Source Code: GitHub
Demo Application: Demo
The Problem
Datatable logic is repeated in many Laravel applications.
Most admin panels and internal tools need similar features:
- Search
- Filters
- Date range filters
- Sorting
- Pagination
- Relationship columns
- Configurable pagination limits
- JSON and custom filters
- Eloquent and Query Builder queries
At first, this logic usually lives directly inside controllers. But after building multiple tables, the same patterns keep repeating.
That was the main reason I wanted to create a package.
Why I Extracted It
The original repository was useful as a project example, but it was not flexible enough to reuse in other applications.
If I wanted the same datatable behavior in another Laravel project, I would need to copy the code manually.
That creates a few problems:
- Duplicate logic
- Harder maintenance
- Inconsistent implementation between projects
- More controller code than necessary
By moving the datatable logic into a package, I can reuse the same backend behavior across different Laravel applications.
Why Backend First?
Even though the package name includes Inertia, it remains backend-first.
It can be used with:
- Inertia
- API resources
- Blade
- JSON responses
- Custom Laravel responses
It does not require Inertia, React, Tailwind, Ziggy, shadcn/ui, or an npm package. Those choices stay in the consuming application.
The backend is where search, filters, sorting, relations, and pagination need to be handled safely and consistently.
Installation
The package requires PHP 8.2 or newer and supports Laravel 10 through 13. Install it using Composer:
composer require raprmdn/laravel-inertia-datatablesPublishing the configuration file is optional:
php artisan vendor:publish --tag=inertia-datatables-configLaravel discovers the service provider and facade automatically.
Basic Usage
ColumnDefinition is now the primary public API. Each definition gives the frontend a public key and maps it to a trusted backend column or relation path.
use App\Models\User;
use Illuminate\Http\Request;
use Raprmdn\DataTables\Column;
use Raprmdn\DataTables\Facades\DataTable;
public function index(Request $request)
{
$users = DataTable::query(User::query())
->columnDefinitions([
Column::group(['name', 'email'])->searchable()->sortable(),
Column::make('status')->filterable()->sortable(),
Column::make('created_at')->dateRange()->sortable(),
])
->applyFilters($request->query('filters', []))
->applySort($request->string('col')->toString() ?: null)
->orderBy('created_at', 'desc')
->make();
return response()->json($users);
}Pagination is the default output. The package reads search, sort direction, and page size from the configured query parameters during make().
For example:
?search=rafi
&filters[]=status:active
&filters[]=created_at_from:01-01-2026
&filters[]=created_at_to:31-01-2026
&col=name
&sort=asc
&limit=25Unknown filter and sort keys are ignored because they do not have an enabled column definition.
Column Definitions
Column::make() creates one public column definition:
Column::make('author', 'author.name')
->searchable()
->filterable()
->sortable();Here, author is the request-facing key and author.name is the trusted backend source. Request input should never be used as a source.
Capabilities are opt-in:
searchable()enables global searchfilterable()enables exact, JSON, or custom filteringsortable()enables requested sortingdateRange()enables_fromand_todate filters
Use Column::group() when multiple columns share the same capabilities:
Column::group([
'name',
'email',
'organization' => 'organization.name',
])->searchable()->sortable();This allowlist keeps public request keys separate from database columns and relation paths.
Filters and Sorting
Filters use column:value strings. Multiple values for one key are grouped with OR, while different keys are combined with AND.
$users = DataTable::query(User::query())
->columnDefinitions([
Column::make('status', 'status_code')->filterable(),
Column::make('organization', 'organization.name')
->filterable()
->sortable(),
Column::make('created_at')->dateRange()->sortable(),
])
->applyFilters($request->query('filters', []))
->applySort($request->string('col')->toString() ?: null)
->orderBy('created_at', 'desc')
->make();Friendly filter values can be mapped to backend values:
Column::make('verification', 'email_verified_at')
->filterable()
->filterAliases([
'verified' => 'NOT NULL',
'unverified' => 'NULL',
]);Date ranges use the configured format, d-m-Y by default. Either boundary can be omitted. The to boundary includes the complete selected day.
If no valid requested sort exists, orderBy() provides the fallback order.
JSON and Custom Filters
JSON scalar paths work like normal filter sources:
Column::make(
'email_notifications',
'settings->notifications->email',
)->filterable();JSON arrays or containment queries use jsonContains():
Column::make('delivery_channel', 'channels')
->filterable()
->jsonContains();Application-specific behavior can use a trusted filterUsing() callback. Custom calculated ordering can use sortUsing(). Both remain behind explicitly enabled filterable() or sortable() definitions.
Relationship Support
Eloquent columns support relationship paths using dot notation.
DataTable::query($query)
->with(['contact.channel', 'priority'])
->withCount('comments')
->columnDefinitions([
Column::group(['number', 'contact.name', 'contact.email'])
->searchable(),
Column::make('priority', 'priority.name')
->filterable()
->sortable(),
])
->make();For example:
'contact.name'means:
contactis the relationship method on the modelnameis the column on the related table
Search, filters, and date ranges support nested Eloquent relation paths. Relation sorting supports BelongsTo and HasOne, including nested and self-referencing relations.
Sorting a HasMany or BelongsToMany relation is ambiguous, so the package throws an InvalidArgumentException. Use sortUsing() when an application needs an explicit aggregation.
Query Builder Support
The same column API works with Laravel Query Builder:
use Illuminate\Support\Facades\DB;
$query = DB::table('users')
->select('users.*')
->leftJoin('organizations as organization', function ($join) {
$join->on('organization.id', '=', 'users.organization_id');
});
$users = DataTable::query($query)
->columnDefinitions([
Column::make('name', 'users.name')->searchable()->sortable(),
Column::make('organization', 'organization.name')
->searchable()
->sortable(),
])
->make();Query Builder does not resolve Eloquent relations. The caller supplies joins and aliases, and dotted sources are treated as SQL table or alias references.
Pagination and Collections
Pagination is the default. The requested limit is clamped between 1 and the configured maximum, which defaults to 100.
Use collection output when pagination is not needed:
DataTable::query(User::query())
->type('collection')
->make();Collection output is unpaginated and ignores page-size settings.
Legacy API
The original parser-based API remains supported for backward compatibility. This includes parseFilters(), parseSort(), searchable(), allowedFilters(), and allowedSorts().
New code should use column definitions. They keep public request keys, trusted backend sources, capabilities, aliases, and callbacks together instead of coordinating separate maps and allowlists.
Current Limitations
The package remains in beta, so the public API may still change before v1.0.0.
Current limitations:
- Frontend components are not included or required
- Relation sorting supports
BelongsToandHasOne, notHasManyorBelongsToMany - Query Builder joins and aliases must be supplied by the caller
- Generic frontend filter operators are not included; application-specific behavior uses
filterUsing() - String column and relation names may not receive complete IDE autocomplete
Final Thoughts
At v0.6.0, the package solves more than the first extracted implementation. It provides one allowlisted API for search, filters, date ranges, sorting, relations, and result output while keeping request-facing keys separate from trusted query sources.
The original project remains available as the complete Laravel and Inertia example application. The package remains backend-first so it can work with Inertia, API resources, Blade, JSON responses, or custom Laravel responses.
Building the feature inside a real project first helped me understand the problem better. After that, extracting it into a package felt much clearer.
Links: