2児のパパ系Webエンジニア
の開発日記
A Dad-of-Two Engineer's Development Diary

Laravel 13のJsonApiResourceでJSON:API対応が標準に!includeでN+1にしないための組み合わせ

Laravel 13のJsonApiResourceでJSON:API対応が標準に!includeでN+1にしないための組み合わせ

🙌 結論から

Laravel 13 なら、JSON:API の形のレスポンスはパッケージなしで作れます!

php artisan make:resource PostResource --json-api で作ったリソースに、返したい属性と関連を並べるだけです!

?include=author や ?fields[posts]=title のようなクエリも、リソース側が見て出し分けてくれます✨

ただ、include で関連を返す時に eager load していないと、一覧で N+1 になりやすいのがハマりどころでした(・_・;)

私は入力側を Spatie の Laravel Query Builder に任せて、allowedIncludes() で eager load まで済ませる形に落ち着きました。

👀 今まではレスポンスの形を自前でそろえていた

社内の API では、JsonResource の toArray() に data と relationships を手で書いて、JSON:API っぽい形にそろえていました。

動くことは動くんですが、リソースが増えるたびに type の書き方や included の組み立てがファイルごとに微妙にずれていくんですよね・・・

レビューのたびに「この API だけ included が無い」みたいな指摘をしていました。笑

Laravel 13 のリリースノートに JSON:API Resources が入っているのを見て、そろそろ自前の形をやめようと思ったのがきっかけです🤔

💡 JsonApiResourceの作り方

作るときは --json-api を付けるだけです💡

php artisan make:resource PostResource --json-api

できたクラスは Illuminate\Http\Resources\JsonApi\JsonApiResource を継承していて、$attributes と $relationships を書く形になっています。

use App\Http\Resources\UserResource;
use Illuminate\Http\Resources\JsonApi\JsonApiResource;

class PostResource extends JsonApiResource
{
    public $attributes = [
        'title',
        'body',
        'created_at',
    ];

    public $relationships = [
        'author' => UserResource::class,
        'comments',
    ];
}

関連の名前だけ書けば、Eloquent のリレーションからリソースクラスを探してくれます(^^)

明示したいときは 'author' => UserResource::class のようにキーとクラスで書けます。

返すときはいつもの new PostResource($post) でも、$post->toResource() でも大丈夫です。

レスポンスの Content-Type も application/vnd.api+json にしてくれるので、ここを自分で付けなくていいのはかなり楽・・!

✨ includeとfieldsでフロントが取りたい物を選べる

関連は、クライアントが include で頼んだ時だけ返ります👀

GET /api/posts/1?include=author,comments
GET /api/posts/1?include=comments.author
GET /api/posts?fields[posts]=title,created_at&fields[users]=name

include を付けると、relationships に id と type だけが入り、中身はトップレベルの included にまとめて入ります。

fields は型ごとに返す属性をしぼる指定で、上の3行目だと posts は title と created_at、users は name だけになります。

一覧画面ではタイトルだけ、詳細画面では本文と作者まで、のようにフロント側で選べるので、画面ごとに API を分けなくて済むようになりました👍

ネストした include には深さの上限があって、変えたいときはサービスプロバイダで JsonApiResource::maxRelationshipDepth(3); のように指定します。

🔧 ハマった点と回避策

ここは真似しやすいように、症状と直し方をセットで書いておきます(`・ω・´)

1. include を付けた一覧でクエリが急に増える

関連は include で頼まれた時に、リソース側で解決されます。

つまり一覧で with() していないと、1件ごとに author や comments を取りに行って N+1 になります(T_T)

私は開発環境だけ preventLazyLoading() を入れているので、ここで例外が出て気づけました!

// AppServiceProvider::boot()
Model::preventLazyLoading(! app()->isProduction());

直し方は、include で頼まれた関連だけ eager load することです。

全部 with() するとムダな関連まで読むので、次の章の Query Builder に任せています🙌

2. type 名がフロントの想定と違った

type はリソースのクラス名から決まります。

公式の説明だと、PostResource は posts、BlogPostResource は blog_posts です。

フロントが blogPosts を想定していて型が合わず、一度ハマりました(T_T)

API を出す前に、toType() で名前を決めておくと安心です👍

public function toType(Request $request): string
{
    return 'articles';
}

外に出す id を連番ではなく UUID にしたい時も、同じように toId() で (string) $this->uuid を返せます。

3. 管理画面用の API では fields を無視したい

管理画面の API でクエリの fields をそのまま受けると、項目が欠けて画面が崩れることがありました(´・ω・`)

そういうレスポンスは ignoreFieldsAndIncludesInQueryString() を付けて、クエリを見ないようにしています。

🔥 私の組み合わせはこれ

Laravel の公式ドキュメントでも、JSON:API のフィルターやソートを読む側には Spatie の Laravel Query Builder が紹介されています。

v7 は2026年3月16日に出ていて、PHP 8.3 以上・Laravel 12 以上が対象です。

allowed* 系のメソッドが配列ではなく可変長引数になっているので、v6 から上げる時はここだけ書き換えが要ります!

私の一覧 API はこんな形です。

use App\Http\Resources\PostResource;
use App\Models\Post;
use Spatie\QueryBuilder\QueryBuilder;

Route::get('/api/posts', function () {
    $posts = QueryBuilder::for(Post::class)
        ->allowedIncludes('author', 'comments')
        ->allowedFilters('title')
        ->allowedSorts('created_at')
        ->get();

    return PostResource::collection($posts);
});

allowedIncludes() に書いた関連だけが ?include= で受け付けられて、その分だけ eager load されます。

あとは JsonApiResource が、読み込み済みの関連を included に出してくれます✨

入力は Query Builder、出力は JsonApiResource、と分けてから、include を足しても N+1 の心配がほぼなくなりました!

🔗 関連記事と使い分け

パパ系Webエンジニア〜愛用ガジェット一覧 🛍️

この記事を書いている作業環境のガジェットを楽天ROOMにまとめています ✨ 在宅エンジニアのデスク作りの参考にどうぞ 🙌

🙌 まとめ

JSON:API の形をそろえたいなら、Laravel 13 の JsonApiResource を使うのが一番手間が少ないです。

--json-api で作って $attributes と $relationships を並べれば、include も fields も Content-Type も面倒を見てくれます。

一方で、include の N+1 と type 名だけは最初に決めておいた方がいいです🔥

自前で JSON:API っぽい形を組み立てていた人は、このやり方無敵じゃない??ってなると思います😊

※ バージョン番号・料金・機能は今後も変わる可能性があります。最新情報は Laravel の公式ドキュメント をご確認ください👀

参考になったらシェアしてもらえるとうれしいです🙌

Xでポスト Threads はてブ LINE