Skip to content

manusiakemos/laravel-tanstack

Repository files navigation

Laravel DataTables for TanStack

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.

About

No description, website, or topics provided.

Resources

License

Stars

1 star

Watchers

0 watching

Forks

Packages

 
 
 

Contributors

Languages