Getting started¶
This page builds the albums type from nothing to a working, validated, related endpoint —
the same AlbumResource the music-catalog workbench ships, layered up one
capability at a time. It assumes you have installed the package. If you are new
to JSON:API itself, read core's
getting-started
first.
1. A model and a resource¶
Start with an Eloquent model — a plain model, nothing JSON:API-specific:
// app/Models/Album.php
final class Album extends Model
{
protected $casts = ['released_at' => 'datetime', 'explicit' => 'boolean'];
}
Then a resource. It extends core's AbstractResource, declares its wire $type, and lists
its fields():
<?php
declare(strict_types=1);
namespace App\JsonApi;
use haddowg\JsonApi\Resource\AbstractResource;
use haddowg\JsonApi\Resource\Field\Id;
use haddowg\JsonApi\Resource\Field\Str;
final class AlbumResource extends AbstractResource
{
public static string $type = 'albums';
public function fields(): array
{
return [
Id::make(),
Str::make('title')->required()->maxLength(200)->sortable(),
];
}
}
Drop it in app/JsonApi/. That is the whole integration: you now have all five CRUD
endpoints under the default server's /api prefix.
GET /api/albums
GET /api/albums/{id}
POST /api/albums
PATCH /api/albums/{id}
DELETE /api/albums/{id}
The package maps the albums type to your Album model by convention — the kebab/plural
type, singularized and studly-cased under the App\Models namespace (configurable as
jsonapi.eloquent.model_namespace) — and auto-registers the reference
Eloquent data layer for every type it can map, so reads and writes work with
no provider wired by hand. When the names diverge, declare the model on the attribute —
#[AsJsonApiResource(model: Album::class)] — and for full control register the
provider/persister pair explicitly, which always wins
(eloquent).
{
"jsonapi": { "version": "1.1" },
"data": [
{ "type": "albums", "id": "1",
"attributes": { "title": "OK Computer" },
"links": { "self": "http://localhost/api/albums/1" } }
]
}
2. More fields, sorting and filtering¶
Core's field DSL covers the
JSON:API vocabulary. storedAs() maps a wire name to a differently-named column;
sortable() opts a field into ?sort; filters() declares the accepted ?filter keys:
use haddowg\JsonApi\Resource\Field\Boolean;
use haddowg\JsonApi\Resource\Field\DateTime;
use haddowg\JsonApi\Resource\Field\Decimal;
use haddowg\JsonApi\Resource\Filter\Contains;
use haddowg\JsonApi\Resource\Filter\DateRange;
public function fields(): array
{
return [
Id::make(),
Str::make('title')->required()->maxLength(200)->sortable(),
Decimal::make('averageRating')->storedAs('average_rating')->readOnly()->nullable(),
DateTime::make('releasedAt')->storedAs('released_at')->sortable(),
Boolean::make('explicit'),
];
}
public function filters(): array
{
return [
Contains::make('title'), // filter[title]=comp
DateRange::make('releasedAt', 'released_at'), // filter[releasedAt][min]=…&filter[releasedAt][max]=…
];
}
An unknown ?sort/?filter key is a 400 by default (strict query parameters — see
configuration). Sorting, filtering, sparse fieldsets and pagination all
work with no further code — the Eloquent provider translates them to the
query builder. Pagination strategies (page / offset / cursor) are on
pagination.
3. A default sort and a validated write¶
Give the collection a stable order when the client sends no ?sort, and let core's declared
constraints validate writes:
use haddowg\JsonApi\Resource\Sort\SortByField;
use haddowg\JsonApi\Resource\Sort\SortDirective;
public function defaultSort(): array
{
return [new SortDirective(SortByField::make('releasedAt', 'released_at'), descending: true)];
}
Str::make('title')->required()->maxLength(200) already means a POST with a missing or
over-long title is a 422:
{ "errors": [ {
"status": "422",
"source": { "pointer": "/data/attributes/title" },
"detail": "The title field is required."
} ] }
The rules are real illuminate/validation rules with real (localizable) messages — see
validation.
4. A relationship¶
Declare a relation and you get its linkage, its self/related links, the related and
relationship endpoints, and ?include — all by convention. The relation name maps to an
Eloquent relationship method on the model:
use haddowg\JsonApi\Resource\Field\BelongsTo;
use haddowg\JsonApi\Resource\Field\HasMany;
public function fields(): array
{
return [
Id::make(),
Str::make('title')->required()->maxLength(200)->sortable(),
BelongsTo::make('artist', 'artists'),
HasMany::make('tracks', 'tracks')->countable(),
];
}
GET /api/albums/1?include=artist,tracks
GET /api/albums/1/tracks # the related endpoint
GET /api/albums/1/relationships/tracks
Relationships — reads, mutations, pivots, ?withCount, and the Relationship Queries profile
— are covered in full on relationships.
5. Where to go next¶
You have a real endpoint. The rest of the docs read in arcs:
- Wiring — resources, capability-composition, configuration, routing.
- The data layer — eloquent, custom-data-providers, pagination.
- Writing — validation, authorization, lifecycle / lifecycle-hooks, actions, atomic-operations.
- Operations — errors, multi-server-and-testing, openapi, optimize.