tomatophp/filament-api
TomatoPHP Filament API adds RESTful endpoints to Filament resources, letting you expose CRUD operations, filters, and relationships via Laravel Sanctum authentication. Generate and manage model APIs directly from your admin panel.
Confirm the preconditions before you composer require anything. You need Laravel 10/11/12, PHP 8.1+, Filament v3 or v4, and Sanctum. The critical precondition is the first one: this package is only useful if Filament is already in the app and already owns the models you want to expose. The package works by introspecting Filament resources. If you don't have them, adopting Filament in order to get an API generator inverts the entire cost calculation — you'd be paying for an admin panel to get a CRUD serializer.
Install
composer require tomatophp/filament-api
# Laravel 11+: Sanctum scaffolding, creates routes/api.php
php artisan install:api
# Take ownership of prefix / middleware / guard
php artisan vendor:publish --tag=filament-api-config
php artisan migrate # personal_access_tokens, if not already present
Read three things, in this order, before you annotate a single resource.
config/filament-api.php. This is the API surface: route prefix, middleware stack, auth guard, and whichever toggles the version you installed exposes. Set the prefix to api/v1 on day one — retrofitting versioning onto a consumed API is churn you don't need.vendor/tomatophp/filament-api/src/, specifically the controller implementing index/show/store/update/destroy. The generated contract is that controller. You need to know how it resolves the model, applies filters, and shapes the response before you promise a payload shape to a mobile client.src/Contracts that tells the package "expose this." Confirm the exact name from the source, not from a blog post; these names have moved between versions.First use case: the most boring resource you own. A Category, Tag, or Country — Eloquent-backed, flat, no relationships, no uploads, no tenancy. Annotate it, issue a token, curl the index endpoint.
// Illustrative shape — confirm the exact contract/trait name in src/Contracts
class CategoryResource extends Resource implements HasApi
{
protected static ?string $model = Category::class;
public static function form(Form $form): Form { /* ... */ }
public static function table(Table $table): Table { /* ... */ }
}
php artisan route:list --path=api # did it register at all? Start here, always.
If that resource round-trips a paginated JSON list, you've proven the mechanism end to end. Everything after that is scoping and hardening, not exploration.
Opt in per resource, deliberately, over weeks — never sweep the panel. Every annotated resource is a contract you now own and a refactor you can no longer make freely. Annotating twelve at once is how you get a surprise breaking change on a Tuesday. Start with reference data, add entities as consumers actually need them.
Treat the Filament form as your request validator, then verify the update path specifically. This is the package's best feature: field lists, required rules, and max: lengths come along for free, which structurally kills the "admin accepts what the API rejects" defect class. But the create and update rules are not identical in Filament, and the generator has to translate between them. Test a PATCH that omits fields before you trust it.
TextInput::make('email')
->required()
->email()
->unique(ignoreRecord: true), // omit this and PATCH will silently reject the record's own email
Own the query explicitly for eager loading. getEloquentQuery() is your hook into the index endpoint, and the index endpoint is your N+1 surface. The table definition may drive relationship display without the generated query preloading anything. Measure with a query log on the first list call, not after launch.
public static function getEloquentQuery(): Builder
{
return parent::getEloquentQuery()->with(['category', 'owner']);
}
Wire authorization yourself and prove it with a test. Do not assume panel policies fire on generated routes — a policy is panel-scoped by default, and the API is not the panel. Either add a policy middleware via the package config, or override the generated controller's methods with $this->authorize(...). Write the 403 test. Apply the same suspicion, doubled, to multi-tenancy: getEloquentQuery() may be tenant-scoped by the panel's current tenant, which a token-authenticated API request does not have. Prove tenant A cannot read tenant B's row.
Issue tokens with abilities, per consumer.
$token = $user->createToken('partner-portal', ['categories:read'])->plainTextToken;
Put auth:sanctum in the middleware config, add throttle:api, and check abilities in a small middleware once you have more than one consumer class. Sanctum abilities are nearly free and they tell you which consumer broke something when something breaks.
Keep a hand-written seam from day one. The generator covers clean CRUD. The moment a consumer needs a computed field, a bulk operation, or a workflow endpoint, you hand-write it. That's fine — but put those routes in a distinct file or controller namespace so the codebase has two clearly labelled route sets rather than a split-brain API with no provenance.
Test generated endpoints exactly like hand-written ones. One feature test per endpoint per resource. This is the highest-leverage habit with an early-stage package: the suite is what makes a future fork safe and what catches a Filament major upgrade before a consumer does.
it('lists categories', function () {
Sanctum::actingAs(User::factory()->create());
$this->getJson('/api/v1/categories')->assertOk()->assertJsonStructure(['data']);
});
Document the contract by hand. Assume no OpenAPI emission ships with this. Hand-author it or point Scribe at the generated routes, and enumerate filter/sort query-string names explicitly — they come from table state and are not self-documenting.
A resource refactor is now an API change. Renaming a field, swapping a form component, or reworking a relationship widget silently changes JSON a mobile app consumes — with no deploy on the consumer side. Add "does this touch an API-exposed resource?" to your PR template. This is the single biggest operational risk of the approach, and it's a process fix, not a code fix.
Filament is a hard coupling. The package introspects Filament internals, so a Filament major version bump is effectively a bump to this package's assumptions, and upstream lag blocks your panel upgrade. Pin both together in composer.json; add an explicit upper bound on Filament if the package doesn't declare one. Plan the pairing as a migration project, not a patch.
The four tests that constitute go/no-go. (1) An unauthorised user gets 403 on a generated route. (2) Tenant A cannot read tenant B's row. (3) A relationship write behaves as expected. (4) An invalid payload returns a sane 422 with per-field errors. Anything you cannot demonstrate with a passing test, you do not have — regardless of what the config appears to say.
Filter and sort query-parameter names are not self-documenting. Filament table filters carry Livewire-shaped state and the names that land on the query string may not match what a consumer would guess. Enumerate them, document them, and treat renaming a filter in the table as a breaking change for anyone who bookmarked it.
This leaks first at file uploads, repeaters, RichEditor, and nested relationship forms. The serialization from Filament form state to JSON is lossy in exactly those places. Test them before promising them. When one fails, hand-write that endpoint — don't fight the generator.
Read the generated store/update controller once, end to end, for mass assignment. Confirm it respects $fillable/$guarded and relationship write guards on the model. Five minutes of reading that prevents an incident.
Rate limiting and CORS are yours. Nothing here implies they're configured. Add throttle:api, include api/* in config/cors.php paths, and document the 429 for consumers.
Debugging sequence when an endpoint misbehaves: php artisan route:list --path=api (registered at all?) → resolved middleware stack → dd() inside the generated controller (what model and form did it resolve?) → query log (what SQL actually ran?). Nine times out of ten it's a resource that never got picked up, or a wrong model binding.
Extension points, in order of preference: (1) config — prefix, middleware, guard; (2) getEloquentQuery() plus the form/table definitions you already control; (3) register your own route ahead of the package's to override a controller method; (4) fork. Forking is genuinely cheap — it's a thin layer over your own Eloquent models — but it must come with a named owner and pinned versions, not a vague intention.
Budget for the bus factor honestly. Single maintainer, low star count, zero dependents, no field evidence of production hardening. Read that as unproven, not broken. Perfectly acceptable for internal ops CRUD and a first-party client on a bounded domain. Disqualifying for a revenue-critical public API unless you've made a deliberate fork-and-own commitment — and your endpoint test suite is the thing that makes that commitment survivable.
How can I help you explore Laravel packages today?