Spec-Compliant APIs Without the Package: Laravel 13's JSON:API Resources
Laravel 13 ships first-party JSON:API resources. See how JsonApiResource handles types, relationships, sparse fieldsets, and includes with less code.
If you have ever built an API against the JSON:API specification, you know the drill. The spec is genuinely good. It standardizes how resources are typed, how relationships are linked, how clients request only the fields they need, and how related records get sideloaded into a single response. The problem was always the implementation. In Laravel you either hand-rolled the envelope in a pile of toArray methods, or you reached for a third-party package and adopted its conventions wholesale. Laravel 13, released in March 2026, closes that gap by shipping JSON:API support directly in the framework.
The centerpiece is a new JsonApiResource class. It extends the familiar JsonResource you already use, so nothing about your mental model changes, but it produces responses that are compliant with the JSON:API spec out of the box. That means resource object structure, relationship linkage, sparse fieldsets, includes, lazy attribute evaluation, and even the correct application/vnd.api+json content type are all handled for you.
Generating a resource
You create one the same way you create any resource, with an added flag:
php artisan make:resource PostResource --json-api
The generated class extends Illuminate\Http\Resources\JsonApi\JsonApiResource and gives you two properties to fill in:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\JsonApi\JsonApiResource;
class PostResource extends JsonApiResource
{
public $attributes = [
// ...
];
public $relationships = [
// ...
];
}
Compared to a traditional resource, where you build the entire response array by hand, this is declarative. You state what the resource exposes and Laravel assembles the compliant document.
Returning a resource
Nothing changes about how you return the resource from a route or controller. You can construct it directly, or lean on the model’s toResource helper that Laravel introduced for all resources:
use App\Models\Post;
Route::get('/api/posts/{post}', function (Post $post) {
return $post->toResource();
});
The response is a proper JSON:API document, with the type, id, and attributes keys the spec requires:
{
"data": {
"id": "1",
"type": "posts",
"attributes": {
"title": "Hello World",
"body": "This is my first post."
}
}
}
Notice the type is posts. Laravel derives it from the resource class name automatically. PostResource becomes posts, and BlogPostResource would become blog-posts. The id comes from the model’s primary key. If you need to override either, you implement toType or toId. That is handy when, for example, an AuthorResource actually wraps a User model but you want the output type to read authors:
public function toType(Request $request): string
{
return 'authors';
}
public function toId(Request $request): string
{
return (string) $this->uuid;
}
Defining attributes
The simplest way to declare attributes is to list them on the $attributes property. Laravel reads each name straight off the underlying model:
public $attributes = [
'title',
'body',
'created_at',
];
When you need computed values or more control, override toAttributes instead. Anything expensive can be wrapped in a closure so it is only evaluated when the attribute actually ends up in the response:
public function toAttributes(Request $request): array
{
return [
'title' => $this->title,
'body' => $this->body,
'is_published' => fn () => $this->published_at !== null,
'created_at' => $this->created_at,
];
}
That lazy evaluation matters more than it looks. With sparse fieldsets, a client might ask for only title. If is_published were a normal array entry it would still run; as a closure it is skipped entirely.
Relationships that behave
This is where the first-party support earns its keep. Relationships are declared, not manually serialized, and they are only included when a client asks for them:
public $relationships = [
'author',
'comments',
];
Laravel resolves the matching Eloquent relationship and discovers the right resource class on its own. If you want to be explicit, use a key and class pair, 'author' => UserResource::class. For full control, override toRelationships and return closures so the relationship payload is only resolved on demand.
A client requests related data through the include query parameter:
GET /api/posts/1?include=author,comments
The response then carries resource identifier objects under relationships and the full records in a top-level included array, exactly as the spec prescribes:
{
"data": {
"id": "1",
"type": "posts",
"attributes": { "title": "Hello World" },
"relationships": {
"author": { "data": { "id": "1", "type": "users" } }
}
},
"included": [
{
"id": "1",
"type": "users",
"attributes": { "name": "Taylor Otwell" }
}
]
}
Nested includes work with dot notation, such as ?include=comments.author. To keep a hostile or careless client from requesting a deeply nested graph, there is a depth cap you can tune in a service provider:
use Illuminate\Http\Resources\JsonApi\JsonApiResource;
JsonApiResource::maxRelationshipDepth(3);
Sparse fieldsets
Sparse fieldsets let clients trim each resource type down to the fields they care about, which keeps payloads small and mobile clients happy:
GET /api/posts?fields[posts]=title,created_at&fields[users]=name
That request returns only title and created_at for posts and name for users. If a particular endpoint should ignore the query string, you can opt out with ignoreFieldsAndIncludesInQueryString() on the resource response.
What it does and does not cover
Worth being clear-eyed here. Laravel’s JSON:API support handles the serialization side, the responses your API sends out. It does not parse incoming JSON:API query parameters for filtering and sorting. For that, the Laravel documentation itself points you at Spatie’s Laravel Query Builder as a companion. So the division of labor is clean: Laravel shapes the output, and you bring a query builder for rich filtering if you need it.
For a working example, imagine the PHP Architect subscriber portal exposing its article feed. A single ArticleResource --json-api, an author relationship, and sparse fieldsets would give mobile and web clients a spec-compliant, bandwidth-aware API with almost none of the envelope code teams used to write by hand.
If you have been putting off adopting JSON:API because the tooling felt heavy, Laravel 13 removes the excuse. It is one flag on an Artisan command and two properties on a class, backed by conventions you already know.