json-api-server is a great way to build APIs in Laravel applications.
To route requests to json-api-server, you will need to obtain an instance of a PSR-7 request instead of a Laravel request. First, install the Symfony HTTP Message Bridge and a compatible implementation:
composer require symfony/psr-http-message-bridge
composer require nyholm/psr7
Then, define a catch-all route for your API and type-hint the request interface on your route closure or controller method. You can then return the response directly – the framework will automatically convert it back into a Laravel response and display it.
use App\Http\Api\Resources;
use Psr\Http\Message\ServerRequestInterface;
Route::get('/api/{uri}', function (ServerRequestInterface $request) {
$api = new JsonApi('/api');
$api->resource(new Resources\PostsResource());
$api->resource(new Resources\UsersResource());
try {
return $api->handle($request);
} catch (Throwable $e) {
return $api->error($e);
}
})->where('uri', '.*');
The Laravel integration provides an EloquentResource
base class which
implements all of the required behaviour for the main endpoints. You can define
Eloquent-backed resources by extending this class, and everything will just
work:
use Tobyz\JsonApiServer\Laravel\EloquentResource;
class PostsResource extends EloquentResource
{
public function type(): string
{
return 'posts';
}
public function newModel(Context $context): object
{
return new Post();
}
public function endpoints(): array
{
return [
Endpoint\Show::make(),
Endpoint\Index::make(),
Endpoint\Create::make(),
Endpoint\Update::make(),
Endpoint\Delete::make(),
];
}
public function fields(): array
{
return [
Field\Attribute::make('title'),
Field\ToOne::make('author')->type('users'),
];
}
}
Eloquent resources will automatically convert field names into snake_case
when
getting and setting model values. For example, a field with the name createdAt
will read its value from the model's created_at
property.
If you need to modify the query used to retrieve and list models – for example,
to only allow the authenticated user to see models that they own, or to select
additional columns – override the scope
method on your resource class:
use Illuminate\Database\Eloquent\Builder;
use Tobyz\JsonApiServer\Context;
class PostsResource extends EloquentResource
{
// ...
public function scope(Builder $query, Context $context)
{
$query->whereBelongsTo(Auth::user());
}
}
This method will also be used to scope queries when retrieving related models for a relationship.
If your Eloquent model is
soft deletable, you can
choose whether or not the soft delete capability is exposed to the JSON:API
client. By default, when a client sends a DELETE
request to remove a resource,
the model will be soft-deleted and it will no longer appear in the API.
Alternatively, you can expose the soft-delete capability to the client, meaning
the client will be able to soft-delete and restore resources via PATCH
requests, and force delete a resource using a DELETE
request.
To expose the soft-delete capability to the client, add the
Tobyz\JsonApiServer\Laravel\SoftDeletes
trait to your Eloquent resource, and a
nullable DateTime
field to your fields
array:
use Tobyz\JsonApiServer\Laravel\SoftDeletes; // [!code ++]
use Tobyz\JsonApiServer\Schema\Field\Attribute; // [!code ++]
use Tobyz\JsonApiServer\Schema\Type\DateTime; // [!code ++]
class PostsResource extends EloquentResource
{
use SoftDeletes; // [!code ++]
// ...
public function fields(): array
{
return [
Attribute::make('deletedAt') // [!code ++]
->type(DateTime::make()), // [!code ++]
->nullable(), // [!code ++]
];
}
}
If you prefer to use a boolean to indicate whether or not a resource is
soft-deleted instead of a nullable date-time value, you can use a
BooleanDateTime
attribute instead:
use Tobyz\JsonApiServer\Schema\Field\BooleanDateTime;
BooleanDateTime::make('isDeleted')
->property('deleted_at')
->writable();
The Laravel integration provides a number of filters for use in your Eloquent resources.
Where::make('name');
Where::make('id')->commaSeparated();
Where::make('isConfirmed')->asBoolean();
Where::make('score')->asNumeric();
WhereBelongsTo::make('user');
Has::make('hasComments');
WhereHas::make('comments');
WhereDoesntHave::make('comments');
WhereNull::make('draft')->property('published_at');
WhereNotNull::make('published')->property('published_at');
Scope::make('withTrashed');
Scope::make('trashed')->scope('onlyTrashed');
The Laravel integration provides a number of sort fields for use in your Eloquent resources.
The SortColumn
class adds a sort field that relates to a database column.
use Tobyz\JsonApiServer\Laravel\Sort\SortColumn;
SortColumn::make('createdAt');
The argument provided to the make
method is the JSON:API sort field name. The
database column is expected to be the underscored version of the field name – so
created_at
in the above example. If you would like to use a different column,
specify it using the column
method:
SortColumn::make('createdAt')->column('created_at');
The SortWithCount
class adds a sort field that relates to a relationship on
the resource's Eloquent model.
For example, to allow the client to sort resources by the number of comments
they have:
use Tobyz\JsonApiServer\Laravel\Sort\SortWithCount;
SortWithCount::make('comments');
If you want the JSON:API sort field name to be different from the relationship
name, specify the relationship name using the relationship
method:
SortWithCount::make('comments')->relationship('approvedComments');
Laravel allows you to alias the relationship count, which is typically used when
there might be a collision with a column name or you are using multiple counts
for the same relationship. Use the countAs
method if you need to alias the
count for the sort:
SortWithCount::make('comments')->countAs('total_comments');
You can also constrain the count by providing a closure to the scope
method.
This receives the query builder instance that is used for the count:
SortWithCount::make('comments')->scope(
fn($query) => $query->where('approved', true),
);
These helpers improve the ergonomics of your API resource definitions when using Laravel.
The authenticated
helper returns a closure which calls Auth::check()
. This
can be used to make endpoints and fields only visible to authenticated users:
use function Tobyz\JsonApiServer\Laravel\authenticated;
Create::make()->visible(authenticated());
The can
helper returns a closure which uses Laravel's
Gate component to check if the
given ability is allowed. If this is used in the context of a specific model
(e.g. on fields and the Show
, Update
, Delete
endpoints), then the model
will be passed to the gate check as well.
use function Tobyz\JsonApiServer\Laravel\can;
Update::make()->visible(can('update'));
::: warning
Before reaching for the rules
helper, remember that you can mark fields as
required and nullable, and make use
of the built-in attribute types and their
options. These will be reflected in your OpenAPI definitions, whereas validation
rules will not.
:::
The rules
helper allows you to use Laravel's
Validation component as a
field validator. Pass a string or array of validation
rules to be applied to the value:
use function Tobyz\JsonApiServer\Laravel\rules;
Attribute::make('password')->validate(rules(['password']));
Note that values are validated one at a time, so interdependent rules such as
required_if
will not work.
You can add an {id}
placeholder to database rules such as unique
which will
be substituted if a model is being updated:
Attribute::make('email')
->type(Str::make()->format('email'))
->validate(Laravel\rules(['email', 'unique:users,email,{id}']));
Validating array contents is also supported:
Attribute::make('jobs')->validate(
Laravel\rules(['required', 'array', '*' => ['string', 'min:3', 'max:255']]),
);
You can also pass an array of custom messages and custom attribute names as the second and third arguments.