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 の心配がほぼなくなりました!
🔗 関連記事と使い分け
- Laravel の N+1 問題を解消した話:
preventLazyLoading()の入れ方はこちら。今回の include の N+1 もこれで気づきました - Laravel 13 の PHP 属性:同じ Laravel 13 の新機能。モデル側の設定は属性、レスポンスの形は JsonApiResource、と役割が分かれています
- Laravel Sanctum で SPA の API 認証:この API を SPA から叩くときの認証まわりはこちらです
パパ系Webエンジニア〜愛用ガジェット一覧 🛍️
この記事を書いている作業環境のガジェットを楽天ROOMにまとめています ✨ 在宅エンジニアのデスク作りの参考にどうぞ 🙌
🙌 まとめ
JSON:API の形をそろえたいなら、Laravel 13 の JsonApiResource を使うのが一番手間が少ないです。
--json-api で作って $attributes と $relationships を並べれば、include も fields も Content-Type も面倒を見てくれます。
一方で、include の N+1 と type 名だけは最初に決めておいた方がいいです🔥
自前で JSON:API っぽい形を組み立てていた人は、このやり方無敵じゃない??ってなると思います😊
※ バージョン番号・料金・機能は今後も変わる可能性があります。最新情報は Laravel の公式ドキュメント をご確認ください👀