Web Development 5-8 minutes

JSON:API in Laravel 13: Build Standard APIs with JsonApiResource, No Packages

Diego Cortés
Diego Cortés
Full Stack Developer & SEO Specialist
Share:
JSON:API in Laravel 13: Build Standard APIs with JsonApiResource, No Packages
Image generated with AI

Laravel 13 speaks JSON:API natively: make:resource --json-api builds resources with type, id, attributes, and relationships per jsonapi.org, no packages. If your API returns a different JSON shape on every endpoint, this tutorial shows the standard that stabilizes your frontend contract.

What Is JSON:API and Why It Standardizes Your Responses

JSON:API is not a framework or a library: it is an open specification maintained by jsonapi.org that defines what a JSON response should look like in a REST API. When your API follows it, any client that knows the spec knows exactly where to find the data, without depending on your team's internal conventions.

The jsonapi.org Spec: type, id, attributes, and relationships

The core rule is that every resource is serialized with a fixed structure: type and id identify it uniquely, its own data lives in attributes, and references to other resources live in relationships. That separation between what a resource is and what it connects to is what makes it possible to request nested relations or partial fields without breaking the contract.

The Problem It Solves: a Different JSON Shape on Every Endpoint

Anyone who has maintained a REST API for a few years knows the scenario: the users endpoint returns { "user": { ... } }, the posts endpoint returns a bare list, and field names change depending on who wrote each controller. The frontend ends up full of special cases. JSON:API removes that ambiguity: the shape of a response stops being a per-endpoint decision and becomes a system-wide rule.

The Content-Type: application/vnd.api+json Header

Part of meeting the spec is declaring the format with the Content-Type: application/vnd.api+json header, both in responses and in requests that carry a body. JSON:API clients use it to detect the format and apply their parsing rules.

Laravel 13 Ships First-Party JSON:API Support

Up to Laravel 12, meeting the spec meant installing a package and learning its particular way of doing things. With Laravel 13 that gap disappears: support arrives integrated into the core.

Before: Third-Party Packages to Meet the Spec

The usual options were packages such as Laravel JSON:API or spatie/laravel-json-api. They worked well, but they added one more dependency, a custom DSL to memorize, and an abstraction layer that sometimes made fine-grained control harder. They filled a gap the framework did not cover.

Now: Native JsonApiResource with make:resource --json-api

Laravel 13 ships first-party JSON:API resource classes. The artisan generator understands the --json-api flag:

php artisan make:resource PostResource --json-api

The command creates a class that extends JsonApiResource instead of the classic JsonResource, and the framework takes care of serializing attributes, relationships, pagination links, and sparse fieldsets according to the spec. The result: your API can be standards-compliant without a single new dependency.

Creating Your First JsonApiResource

The starting point of any JSON:API endpoint in Laravel 13 is one resource per model. Let us create the one for a blog's posts.

The Artisan Command and the Generated File Structure

The generated file lands in app/Http/Resources and its skeleton is intentionally small: you declare which attributes you expose and which relationships exist, and Laravel decides how to serialize them. That separation keeps the code clean as the model grows.

Defining Attributes with the attributes() Method

The attributes the client will see are declared in attributes():

public function attributes(Request $request): array
{
    return [
        'title' => $this->title,
        'slug' => $this->slug,
        'body' => $this->body,
        'published_at' => $this->published_at,
    ];
}

You decide explicitly which fields go out: internal dates, foreign keys, or technical columns stay out simply by not listing them. If you need to hide a field later, you remove it here and the change propagates to every endpoint that uses the resource.

Resource Type and ID: What Identifies Your Resource

Each resource declares a type (for example posts) that identifies it among other types, and the id maps to the model's primary key. Clients use that type+id pair to cache resources and resolve relationship references, so it pays to keep the type stable from day one: changing it later means updating the whole frontend.

Relationships: Includes Without N+1

One of the biggest wins of JSON:API is that the client decides when related resources travel in the same response, through the include parameter. In Laravel 13 that translates into declaring the relationships on the resource.

Defining Relationships in the Resource

In relationships() you state which relations the client may include and how to serialize them:

public function relationships(Request $request): array
{
    return [
        'author' => fn () => new UserResource($this->author),
        'comments' => fn () => CommentResource::collection($this->comments),
    ];
}

The closures are evaluated only if the client asks for the relation, so a simple request does not pay the cost of serializing the whole object graph.

Loading Relationships with ?include=author

With that declaration, a request such as ?include=author returns the post with its author embedded in the same document, and ?include=author.comments works for nested relations. The result is a compound document: a single response, with no cascading extra requests.

Combining Includes with Eager Loading in the Controller

The client asking for a relation does not mean Eloquent loads it on its own: the controller is still responsible for avoiding the N+1 problem. The pattern is identical to usual, as I explain in my guide on eliminating the Eloquent N+1 problem, except that here the list of relations to preload can come straight from the request's include.

Sparse Fieldsets: Let the Client Ask for Only What It Uses

The spec lets the client limit which attributes it receives, per resource type. It is the perfect tool for listings that only show two or three fields.

The ?fields[posts]=title,body Parameter

With a request such as ?fields[posts]=title,body, the API responds with only those two attributes for each post, ignoring the rest even if they are declared in attributes(). Because the filter applies per type, you can ask for different fields for posts, authors, and comments in the same call.

Less Bandwidth and Faster Responses

In a mobile app or a dashboard that lists hundreds of rows, trimming heavy payloads makes a real difference in load time and data usage. Sparse fieldsets turn that optimization into something the client decides rather than the backend, which simplifies things when the same API feeds both a desktop web app and a mobile app.

Pagination Links and Meta

JSON:API collections do not return just a list: they include pagination information in a standard way, so the client can move through pages without custom conventions.

Pagination with first, last, next, and prev Links

When the controller paginates the query with paginate(), the collection resource adds the first, last, next, and prev links automatically. The frontend consumes them directly, without parsing headers or guessing the URL format of the next page.

Resource Meta Information

Alongside the links, the response includes meta with useful pagination data such as the total number of items or the current page. If your API needs to expose other global information, the spec leaves room to extend that meta block in a controlled way.

Classic JsonResource vs JsonApiResource

First things first: the classic JsonResource does not disappear in Laravel 13. It is still generated with plain make:resource and it remains the right choice in many cases. The question is not which one is better, but when to use each.

When to Keep Using JsonResource

The classic resource gives you total control: data wrapping, conditional attributes with when(), conditional relations with whenLoaded(), and the freedom to return whatever exact JSON you need. For internal APIs serving a single application, ad hoc responses, or legacy endpoints you cannot touch, it is still the most direct tool.

Migrating an Existing API Endpoint by Endpoint

If you already have an API built with JsonResource, you do not need to rewrite it over a weekend. The migration happens endpoint by endpoint: create the JsonApiResource for a model, change that endpoint's response, run your tests, and repeat. The rest of the API keeps working with its previous format, and every migrated endpoint reduces the consistency debt.

Spatie Laravel Query Builder as a Companion for Filters and Sorting

JSON:API standardizes the output, but it does not parse input parameters such as filters or sorting. Laravel's official docs recommend pairing it with Spatie Laravel Query Builder to translate those parameters into Eloquent queries; if you work with more complex queries, you may also like my guide on advanced Query Builder in Laravel 13.

Real-World Case: a Posts API with Author and Comments

Let us put it all together with the classic example: a Laravel 13 API protected with Sanctum for a blog, moving from inconsistent responses to standard JSON:API.

GET /api/posts and GET /api/posts/{post} Endpoints

You define the resources (PostResource, UserResource, and CommentResource with --json-api) and preload in the controller the relations the client may ask for:

Route::get('/api/posts', function () {
    return PostResource::collection(
        Post::query()->with(['author', 'comments'])->paginate()
    );
});

Sanctum protection stays exactly as it was; only the serialization layer changes. If you need context on hardening those endpoints, the article on rate limiting for Laravel 13 APIs is a good companion.

Verifying the Final Response with curl

A request that asks for relations and partial fields looks like this:

curl -H "Accept: application/vnd.api+json" \
  "https://api.example.com/api/posts?include=author&fields[posts]=title,body"

And the response follows the spec structure: each post with its type, its id, the filtered attributes, and the author relation resolved in the same document. That is the stable contract your React, Vue, or mobile frontend can consume without special cases.

Conclusion

Native JSON:API in Laravel 13 removes the excuse that meeting the spec requires too much infrastructure: with one artisan flag, one method for attributes, and one for relationships, your API responds in a standard format any client understands. Start with a single endpoint, watch it work with sparse fieldsets and includes, and let consistency spread to the rest. If this tutorial helped you, keep reading the blog for more Laravel 13, API, and web development guides.

Categories