laravel-tanstack maintained by manusiakemos
Laravel DataTables for TanStack
Modern server-side datatable for Laravel, purpose-built for TanStack Table frontends (React, Vue, Svelte, Solid). Inspired by yajra/laravel-datatables but with a clean REST API instead of the legacy datatables.net protocol.
Use this when you have an Inertia (or any SPA) frontend, want server-side processing, and want to stop writing pagination + search + sort + filter boilerplate for every table.
Why this package
- Three-line endpoints. Whitelist columns, return the response. That's it.
- REST-style query string. Clean, browser-DevTools-friendly, easy to cache.
- Type-safe FE companion. Pairs with
@manusiakemos/laravel-tanstack-react(separate package). - Eloquent or Query Builder. Both supported, no extra setup.
- No jQuery, no datatables.net baggage.
Installation
composer require manusiakemos/laravel-tanstack
Publish the config (optional):
php artisan vendor:publish --tag=laravel-tanstack-config
Quick start
use Manusiakemos\LaravelTanstack\DataTable;
use App\Models\User;
class UserDataTableController
{
public function __invoke(Request $request)
{
return DataTable::for(User::query())
->searchable(['name', 'email'])
->sortable(['name', 'email', 'created_at'])
->filterable(['status', 'role']);
}
}
Register it on a web route so it has access to the session (for Inertia auth):
Route::get('/datatable/users', UserDataTableController::class)->middleware('auth');
Done. The controller returns a JsonResponse automatically because DataTable implements Responsable.
API protocol
Request
GET /datatable/users?
page=1&
per_page=25&
sort=name:asc,created_at:desc&
search=hafiz&
filter[status]=active&
filter[role][]=admin&filter[role][]=editor
| Param | Description |
|---|---|
page |
1-indexed page number. Defaults to 1. |
per_page |
Rows per page. Clamped to max_per_page config. |
sort |
Comma-separated column:direction pairs. |
search |
Global search term across searchable() columns. |
filter[col]=v |
Equals filter. |
filter[col][]=v1&filter[col][]=v2 |
whereIn filter. |
Response
{
"data": [
{ "id": 1, "name": "Hafiz", "email": "hafiz@example.com" }
],
"meta": {
"page": 1,
"per_page": 25,
"total": 1234,
"filtered": 89,
"last_page": 4
}
}
Features
Transform rows
DataTable::for(User::query()->with('role'))
->transform(fn ($user) => [
'id' => $user->id,
'name' => $user->name,
'role' => $user->role->name,
'status_label' => $user->status === 'active' ? 'Active' : 'Inactive',
]);
Or use an API Resource:
DataTable::for(User::query())->resource(UserResource::class);
Custom search
DataTable::for(User::query())
->search(fn ($q, $term) =>
$q->whereRaw('LOWER(name) LIKE ?', ['%'.strtolower($term).'%'])
->orWhereHas('role', fn ($r) => $r->where('name', 'like', "%{$term}%"))
);
Custom sort column
For computed or relation columns:
DataTable::for(User::query())
->sortable(['name', 'role_name'])
->orderColumn('role_name', fn ($q, $dir) =>
$q->orderBy(
Role::select('name')->whereColumn('roles.id', 'users.role_id'),
$dir
)
);
Custom filter
DataTable::for(User::query())
->filterColumn('created_between', fn ($q, $value) =>
$q->whereBetween('created_at', explode(',', $value))
);
Request: ?filter[created_between]=2024-01-01,2024-12-31
Authorization
DataTable::for(User::query())
->authorize(fn () => Gate::allows('viewAny', User::class));
Returns 403 if the closure returns false.
Default sort
DataTable::for(User::query())
->sortable(['created_at'])
->defaultSort('created_at', 'desc');
Skip total count
For very large tables where count(*) over the unfiltered set is expensive:
DataTable::for(User::query())->skipTotal();
The response will have meta.total = null; frontend should rely on filtered only.
Pagination limits
DataTable::for(User::query())
->defaultPerPage(50)
->maxPerPage(200);
Query builder macro
For one-liner usage:
return User::query()
->where('active', true)
->toDataTable()
->searchable(['name'])
->sortable(['name']);
Inertia + TanStack pattern
The recommended setup: let Inertia render the page shell (layout, auth state, navigation), and let the table component fetch its own data from a separate JSON endpoint.
// Page rendered by Inertia
import { useDataTable } from '@manusiakemos/laravel-tanstack-react'
function UsersIndex() {
const { table, loading } = useDataTable<User>({
endpoint: '/datatable/users',
columns: [
{ accessorKey: 'name', header: 'Name' },
{ accessorKey: 'email', header: 'Email' },
],
})
return <DataTableView table={table} loading={loading} />
}
The table state lives in React; the page shell stays Inertia-managed. No full-page reload on pagination.
Security notes
- Never call
->searchable()or->sortable()with untrusted column names. The package enforces whitelisting — only listed columns can be searched/sorted — but you must define the list yourself. - Use
->authorize()for any non-public table. Or check authorization in the controller before returning the DataTable. - The package is read-only — it does not perform writes, so SQL injection surface is limited to filter values which are bound parameters.
Configuration
The full config/laravel-tanstack.php:
return [
'default_per_page' => 25,
'max_per_page' => 100,
'case_insensitive' => true,
'report_exceptions' => true,
];
Testing
composer test
Requirements
- PHP 8.2+
- Laravel 11.x or 12.x
Roadmap
- PostgreSQL-specific
ILIKEsearch optimization - Laravel Scout integration for full-text search
- Vue 3 and Svelte FE companions
- Excel/CSV export endpoint
- Saved view / preset support
Contributing
Contributions are welcome — bug reports, feature requests, and pull requests.
Before opening a PR:
- Fork the repo and create a feature branch from
main:git checkout -b feat/short-description - Install dependencies:
composer install - Make your change. Keep the public API stable unless the PR is explicitly a breaking change.
- Add or update tests under
tests/Featureortests/Unit. New features without tests will not be merged. - Run the full quality gate locally — it must pass before you push:
composer format # Laravel Pint composer analyse # PHPStan composer test # Pest - Update
CHANGELOG.mdunder the[Unreleased]section. Use the Keep a Changelog categories:Added,Changed,Deprecated,Removed,Fixed,Security. - Open the PR against
mainwith a clear description of the change, the motivation, and any breaking-change notes.
Branch naming: feat/..., fix/..., docs/..., refactor/..., test/....
Commit style: Conventional Commits preferred (feat:, fix:, docs:, chore:, refactor:, test:).
Issues: When filing a bug, include the Laravel version, PHP version, a minimal reproduction, and the actual vs expected behavior. For feature requests, describe the use case before the proposed API.
License
MIT.