【Laravel】LaravelでAPIを作る基本(API Resource)

laravelアイキャッチ Laravel

こんにちは、かつコーチです。

前回はLivewireの基本機能を解説しました。

今回は少し話題を変えて、フロントエンドがVueやReact、あるいはスマホアプリになったときに欠かせない「API」の作り方を扱います。

Eloquentモデルをそのまま返すのではなく、レスポンスの形を整える「API Resource」の使い方を、実際につまずいたポイントも交えて解説していきます。

API Resourceとは?

モデルをそのまま返す方法の問題点

LaravelでAPIを作る最初の一歩として、こんなコードを書いたことがある人も多いのではないでしょうか。

// app/Http/Controllers/Api/PostController.php
namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\Post;

class PostController extends Controller
{
    public function index()
    {
        return Post::with('user')->get();
    }
}

このコードは動きはしますが、実際に運用していくといくつかの問題が出てきます。

  • passwordremember_token のような、外部に見せたくないカラムまでそのまま返ってしまう
  • リレーション(user など)の構造がモデルの実装に依存し、フロント側の期待する形と食い違いやすい
  • レスポンスの形を変えたいとき、コントローラのあちこちに手を入れる必要が出てくる

API Resourceは、こうした「モデルとAPIレスポンスの形を分離する」ための仕組みです。

API Resourceの役割

API Resourceを一言でいうと、モデルのデータをAPIレスポンス用に変換する専用クラスです。

コントローラは「データを取得する」ことに専念し、「どんな形でレスポンスするか」はAPI Resourceの責務にする、という役割分担ができます。

この分離によって、モデルの構造が変わってもAPIの見た目を維持できたり、逆にAPIの形だけを変えたいときにモデルへ手を入れずに済んだりするメリットが生まれます。

API Resourceの作成と基本の使い方

Artisanコマンドで作成する

Resourceクラスは make:resource コマンドで作成します。

php artisan make:resource PostResource

app/Http/Resources/PostResource.php が生成されるので、toArray() メソッドの中でレスポンスの形を定義します。

// app/Http/Resources/PostResource.php
namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class PostResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'title' => $this->title,
            'body' => $this->body,
            'author_name' => $this->user->name,
            'published_at' => $this->published_at?->format('Y-m-d'),
        ];
    }
}

コントローラ側では、モデルを取得したあとにResourceでラップして返します。

// app/Http/Controllers/Api/PostController.php
namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Http\Resources\PostResource;
use App\Models\Post;

class PostController extends Controller
{
    public function show(Post $post)
    {
        return new PostResource($post->load('user'));
    }
}

このAPIにアクセスすると、次のようなJSONが返ってきます。

{
    "data": {
        "id": 1,
        "title": "Livewireの基本を解説",
        "body": "こんにちは、かつコーチです。",
        "author_name": "かつコーチ",
        "published_at": "2026-08-11"
    }
}

password のような内部用のカラムはレスポンスに含まれず、author_name のように必要な形に整形されたデータだけが返っている点がポイントです。

一覧を返すコレクションの場合

複数件のデータを返す場合は collection() を使います。

public function index()
{
    $posts = Post::with('user')->latest()->paginate(20);

    return PostResource::collection($posts);
}

paginate() の結果をそのまま渡すと、data の中身が整形されつつ、ページネーション情報(linksmeta)も自動的にレスポンスへ含まれます。

{
    "data": [
        { "id": 2, "title": "...", "author_name": "..." },
        { "id": 1, "title": "...", "author_name": "..." }
    ],
    "links": { "first": "...", "last": "...", "prev": null, "next": "..." },
    "meta": { "current_page": 1, "last_page": 3, "total": 45 }
}

一覧APIを実装するたびにページネーション情報を手で組み立てる必要がなくなるのは、実務上かなり助かるポイントです。

つまずきやすいポイント:N+1問題と条件分岐の書き方

Resource内でリレーションを呼んでN+1が発生した話

私が実際に業務でハマったのが、Resourceクラスの中でリレーションに気軽にアクセスしたことによるN+1問題(1件ごとに追加のクエリが発行され、件数分だけSQLが増えてしまう問題)です。

❌ Before:コントローラでeager loadを忘れたままResourceでリレーションにアクセスする

// コントローラ側
public function index()
{
    $posts = Post::latest()->paginate(20); // withを付け忘れている

    return PostResource::collection($posts);
}
// Resource側
public function toArray(Request $request): array
{
    return [
        'id' => $this->id,
        'title' => $this->title,
        'author_name' => $this->user->name, // ここでリレーションを呼んでいる
    ];
}

一見普通に動いているように見えるのですが、投稿が20件あれば $this->user->name へのアクセスのたびに追加でSQLが1回ずつ発行され、合計21回のクエリが実行されてしまいます。

開発環境ではデータ件数が少なく気づかず、本番環境で投稿数が増えてから管理画面の表示が急に遅くなり、Laravel Debugbarでクエリ数を確認して初めて原因に気づいた、という経験があります。

✅ After:コントローラでeager loadしておき、Resourceは受け取ったデータを整形するだけにする

// コントローラ側
public function index()
{
    $posts = Post::with('user')->latest()->paginate(20);

    return PostResource::collection($posts);
}
// Resource側(変更なし)
public function toArray(Request $request): array
{
    return [
        'id' => $this->id,
        'title' => $this->title,
        'author_name' => $this->user->name,
    ];
}

Resourceクラスの中でリレーションにアクセスする設計にする場合は、「呼び出し元(コントローラ)で必ずeager loadする」というルールをチームで徹底しておくのが安全です。

不安な場合は、開発環境で DB::listen() やLaravel Debugbarを使い、実際に発行されるクエリ数を確認する習慣をつけると早期に気づけます。

条件によってフィールドを出し分ける

管理者だけに見せたい項目がある場合、when() メソッドを使うと条件付きでフィールドを出し分けられます。

❌ Before:if文でtoArray全体を分岐させる

public function toArray(Request $request): array
{
    if ($request->user()?->is_admin) {
        return [
            'id' => $this->id,
            'title' => $this->title,
            'author_name' => $this->user->name,
            'author_email' => $this->user->email,
        ];
    }

    return [
        'id' => $this->id,
        'title' => $this->title,
        'author_name' => $this->user->name,
    ];
}

共通のフィールドが重複してしまい、フィールドを追加するたびに2箇所を修正しないといけない状態になります。

✅ After:when()で条件付きフィールドだけを出し分ける

public function toArray(Request $request): array
{
    return [
        'id' => $this->id,
        'title' => $this->title,
        'author_name' => $this->user->name,
        'author_email' => $this->when(
            $request->user()?->is_admin,
            $this->user->email
        ),
    ];
}

when() の第1引数がfalseの場合、author_email のキー自体がレスポンスから除外されます。

共通部分を重複させずに条件分岐を書けるため、フィールドが増えてきたときの保守性が大きく変わります。

一歩先の使い方:エラーレスポンスの統一

バリデーションエラーの形を整える

API用のバリデーションでは、失敗時のレスポンス形式もフロント側と事前にすり合わせておくと後々のトラブルを防げます。

// app/Http/Requests/StorePostRequest.php
namespace App\Http\Requests;

use Illuminate\Contracts\Validation\Validator;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\Exceptions\HttpResponseException;

class StorePostRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'title' => 'required|string|max:100',
            'body' => 'required|string|max:5000',
        ];
    }

    protected function failedValidation(Validator $validator): void
    {
        throw new HttpResponseException(response()->json([
            'message' => '入力内容に誤りがあります',
            'errors' => $validator->errors(),
        ], 422));
    }
}

FormRequestfailedValidation() をオーバーライドすることで、バリデーション失敗時のレスポンス形式をプロジェクト全体で統一できます。

APIのバリデーションエラーは頻繁に発生するため、フロント側が処理しやすい一貫した形にしておくことが、後々の実装コストを大きく減らしてくれます。

まとめ

この記事のポイント

  • API Resourceは、モデルとAPIレスポンスの形を分離するための専用クラス
  • make:resource で作成し、toArray() の中でレスポンスの形を定義する
  • collection() を使うとページネーション情報も含めて自動的に整形される
  • Resource内でリレーションにアクセスする場合は、コントローラ側でのeager loadを忘れない
  • when() を使うと重複を避けつつ条件付きでフィールドを出し分けられる

次に読むべき記事

API Resourceで自社のAPIを整形する方法がわかったところで、次回は逆に外部のAPIをLaravelから呼び出す方法を解説します。

→ 次の記事:外部APIとの連携方法(Http::get/postの使い方)

タイトルとURLをコピーしました