manusiakemos/laravel-tanstack

Modern server-side datatable for Laravel, designed for TanStack Table frontends. Drop-in query builder with searching, sorting, filtering, and pagination.

Maintainers

Package info

github.com/manusiakemos/laravel-tanstack

pkg:composer/manusiakemos/laravel-tanstack

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 1

Open Issues: 0

0.1.0 2026-05-27 07:25 UTC

This package is auto-updated.

Last update: 2026-05-27 09:39:01 UTC


README

Latest Version Tests License

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 ILIKE search 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:

  1. Fork the repo and create a feature branch from main:
    git checkout -b feat/short-description
  2. Install dependencies:
    composer install
  3. Make your change. Keep the public API stable unless the PR is explicitly a breaking change.
  4. Add or update tests under tests/Feature or tests/Unit. New features without tests will not be merged.
  5. Run the full quality gate locally — it must pass before you push:
    composer format    # Laravel Pint
    composer analyse   # PHPStan
    composer test      # Pest
  6. Update CHANGELOG.md under the [Unreleased] section. Use the Keep a Changelog categories: Added, Changed, Deprecated, Removed, Fixed, Security.
  7. Open the PR against main with 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.