tenantcloud/php-json-api
A Laravel package for building JSON:API compliant APIs, handling resource serialization, sparse fieldsets, filtering, sorting, pagination, and relationship includes so you can return spec-ready responses with minimal boilerplate.
composer require tenantcloud/laravel-json-api and publish the package config (php artisan vendor:publish --tag=...) so you can see the default resource/serializer wiring before you write anything.type names, and endpoints lives. Start there rather than at the controllers, because most behaviour is driven by that mapping.application/vnd.api+json. A quick smoke test is a single GET /api/posts?filter[status]=published&include=author&fields[posts]=title,author — if you get a data array with type/id/attributes/relationships, the plumbing is correct.errors document (400) for unknown filters/sorts rather than silently ignoring them. This is the single biggest source of "why is my API behaving oddly" tickets.ids, date ranges, comma-separated lists). As of 7.0.2, filter input is normalised in every request bag, so your handler receives the same shape whether the parameters arrived as a query string, a test-built request, or a nested array — write handlers against the normalised value and don't re-parse raw bags yourself.filter[ids]=1,2,3, filter[published-at][gte]=..., filter[author]=5 — implement these as small dedicated filter classes so they're unit-testable and reusable across resources.include= and fields[type]= on top. If a relationship is expensive, keep it out of the default include set and require an explicit include.errors with source.pointer pointing at the offending query parameter, not an HTML 422.Accept: application/vnd.api+json, assert on the document structure (assertJsonPath('data.0.type', 'posts')), and add one test per filter handler asserting the SQL/collection result and the error returned for an invalid filter value.Accept/Content-Type yields 406/415 before your code runs. Add the header to your HTTP client's defaults in tests and in the frontend's API client, not per-call.$request->query->all()['filter'] directly instead of the normalised parameters are the ones most likely to need attention.=== compare a document id against an integer model key; cast deliberately.filter[x]= with an empty value — decide explicitly whether that means "no filter" or "equals empty string". It's a common source of accidental full-table returns.include=; also guard against unbounded includes on large relationship collections by paginating relationship endpoints.Request when a filter or sort "doesn't work" — most confusion is a naming mismatch (filter[status] vs filter[state]) or an un-declared allowed filter.meta/links hook — these are the places you'll otherwise end up copy-pasting across a dozen resources.How can I help you explore Laravel packages today?