cloudcreativity/laravel-json-api
A Laravel package for building JSON:API compliant APIs with Eloquent support, offering resource routing, validation, filtering, sorting, pagination, sparse fieldsets, and relationship includes out of the box.
Install the package (it targets Laravel 5.5–7.x; verify the composer.json constraints before adding it to a modern app):
composer require cloudcreativity/laravel-json-api
Publish the config and register your first API under apis:
php artisan vendor:publish --provider="CloudCreativity\LaravelJsonApi\ServiceProvider"
// config/json-api.php
'apis' => [
'v1' => [
'name' => 'My API',
'prefix' => 'api', // routes become /api/...
'version' => 'v1', // .../v1/...
'adapter' => 'eloquent',
'schemas' => [
'posts' => App\JsonApi\V1\Posts\Schema::class,
],
],
],
Generate a resource schema (the package's generators live under make:json-api:*):
php artisan make:json-api:resource posts --api=v1
# also: make:json-api:schema, make:json-api:validator, make:json-api:controller, make:json-api:filter
Define the schema — point it at a model and list fields:
class Schema extends AbstractSchema
{
protected $model = Post::class;
protected $resourceType = 'posts';
public function fields()
{
return [
$this->id(),
$this->attribute('title'),
$this->attribute('content'),
$this->attribute('created-at', 'created_at'), // JSON key => column
$this->hasOne('author'),
$this->hasMany('comments'),
];
}
}
Register routes in a service provider (usually RouteServiceProvider::boot):
JsonApi::register('v1')->routes(function ($api) {
$api->resource('posts')->relationships('author', 'comments');
$api->resource('comments')->readOnly();
});
First use case: expose an existing Eloquent model as a read-only resource, then bolt on filters, sorting, pagination and include support. You do not write controllers — the package supplies the JSON:API controller stack and turns Eloquent into a JSON:API document.
Call it correctly. Every request must send Accept: application/vnd.api+json (and Content-Type: application/vnd.api+json for writes). Browse /api/v1/posts?include=author&fields[posts]=title,author&page[size]=10 to confirm end-to-end behaviour.
Schema-centric design. Almost all per-resource behaviour lives in one schema class: fields(), relationships(), filters(), sort(), pagination(), and validator wiring. Treat the schema as the source of truth; keep models thin and avoid sprinkling JSON:API logic into controllers.
Relationships and includes. Declare hasOne, hasMany, belongsToMany, and expose relationship routes via ->relationships('author', 'comments'). Clients then use ?include=author,comments.user. Sparse fieldsets (?fields[posts]=...) and includes are whitelisted from what you declare — anything not declared returns 400.
Filtering and sorting. Return filter objects from filters(); the package ships filters such as StrFilter, WhereIn, and WhereHas, and you can write a custom AbstractFilter. Sorting is a whitelist array (['title', '-created-at'] where - means descending); unknown sort keys are rejected with 400.
Pagination. Defaults to page-based pagination (page[number], page[size]) driven by pagination() in the schema. Set a sane default and max page size there; return pagination links/meta automatically.
Validation. Generate validator classes (make:json-api:validator) extending the package's AbstractValidators and implement rules(), messages(), and attributes(). Rules apply to the JSON:API attributes/relationships payload, not the raw HTTP body. Wire them into the schema so create/update are validated before the adapter runs; validation failures are converted to JSON:API 422 error documents.
Adapter hooks. Extend the Eloquent adapter when the default CRUD isn't enough. Override query() to add eager loads/global scopes, search() for custom query-string handling, and create()/update()/delete() for side effects (slugs, file handling, events). Return domain objects from hooks rather than manipulating responses.
Authorization. Use ordinary Laravel policies (viewAny, view, create, update, delete); the package maps JSON:API actions onto Gate checks. Register policies normally — there's no separate ACL layer.
Transactions. Enable 'transactions' => true so writes are wrapped in a DB transaction. Useful for relationship updates that touch pivot tables; keep jobs/events deferred until after commit if they depend on the write.
Errors. Throw JsonApiException (or let ModelNotFoundException/ValidationException bubble up) and let the package render a spec-compliant errors array. Add 'debug' => env('JSON_API_DEBUG', false) locally to include traces in error meta.
Testing. Use Laravel's HTTP test helpers but override the Accept header explicitly:
$this->withHeader('Accept', 'application/vnd.api+json')
->getJson('/api/v1/posts?include=author')
->assertOk()
->assertJsonPath('data.0.type', 'posts');
Accept: application/vnd.api+json yields 406 Not Acceptable, which surprises clients, Postman users, and browser devs. Add the header to every frontend/API client and to every test.laravel-json-api/laravel. For new projects, strongly prefer the successor; for legacy apps, pin versions and audit Laravel upgrade compatibility before committing.POST, add id to your validation rules and handle it in the adapter's create().query() applies with() for requested include paths; otherwise ?include=author,comments.user is a guaranteed N+1. Use $request/search hook to inspect include paths and eager-load accordingly.sort, filter, or fields[...] that isn't declared is a 400, not silently ignored. This is a feature — document the whitelist for frontend teams.GET /posts/1/author (related resource) and GET /posts/1/relationships/author (relationship linkage) are distinct. Writes to relationships use PATCH .../relationships/author with a data object, not a normal PUT/PATCH on the resource.JsonApi::register() will break route:cache. Keep custom routes in a controller-backed file.php artisan config:cache) is fine, but the apis array is read at boot — clear cache after editing schemas, and remember that adding a schema class means editing config, not just adding a file.ValidationException thrown from a custom middleware or a non-JSON:API controller returns Laravel's default JSON; route all API traffic through the package's middleware group.AbstractAdapter, custom AbstractFilter, custom field classes (for computed attributes), and a custom Store implementation if you're not on Eloquent. Prefer these over monkey-patching the schema.400/406, check the errors[].detail and source.pointer fields first — they usually identify the unrecognized field, filter, or sort key immediately.How can I help you explore Laravel packages today?