API Resources

A resource class is the transformation layer between an Eloquent model and the exact JSON your API has promised.

Syntaxphp artisan make:resource PostResource # app/Http/Resources/PostResource.php

Returning a model from a controller works, because Eloquent models serialise themselves to JSON. It also publishes every column you add to the table later and every column you forgot to hide. An API resource is a class that maps a model to the array you have promised, and nothing else. Generate one with php artisan make:resource PostResource. It lands in app/Http/Resources, extends Illuminate\Http\Resources\Json\JsonResource, and defines a single method, toArray(Request $request): array.

Where $this comes from

Inside toArray(), $this->title is not a property of the resource. JsonResource forwards unknown property and method access to the model it wraps, so the model's attributes, casts, accessors and relations all read as though they were the resource's own.

Collections and wrapping

PostResource::collection($posts) runs every model through the resource. The outermost resource is wrapped in a data key. JsonResource::withoutWrapping() in a service provider's boot() removes that key, except on paginated responses, which keep data because they also carry meta and links.

Shorthand

$post->toResource() is shorthand for new PostResource($post), and $posts->toResourceCollection() for PostResource::collection($posts). The short forms locate the class by convention: a Post model looks for PostResource in the Http\Resources namespace nearest the model. Laravel 13 also offers php artisan make:resource PostResource --json-api, which generates a JsonApiResource that emits the JSON:API document shape and sets Content-Type: application/vnd.api+json.

Example

Example · php
<?php
// app/Http/Resources/PostResource.php

declare(strict_types=1);

namespace App\Http\Resources;

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

class PostResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     *
     * @return array<string, mixed>
     */
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'title' => $this->title,
            'slug' => $this->slug,
            'excerpt' => Str::limit($this->body, 120),

            // published_at is cast to a date on the model, so it is a Carbon
            // instance here. ?-> keeps a draft post from blowing up.
            'published_at' => $this->published_at?->toIso8601String(),

            // Included only if the controller eager loaded the relation.
            'author' => new UserResource($this->whenLoaded('author')),

            // Included only after ->withCount('comments') or ->loadCount('comments').
            'comment_count' => $this->whenCounted('comments'),

            // Included only for the owner. The key is dropped for everyone else.
            'view_count' => $this->when(
                $request->user()?->id === $this->user_id,
                fn (): int => $this->view_count,
            ),
        ];
    }
}

When to use it

  • Keep a newly added internal column out of a response that third parties already consume.
  • Rename a database column without changing the field name your clients read.
  • Include an author object only on the endpoints that eager load the relation.
  • Return 201 with a Location header after creating a record.

More examples

Returning the resource from a controller

The controller decides what is loaded; the resource decides what is published. That split is the point: the resource never queries, so it cannot cause an N+1, and a route that forgets with('author') simply omits the key.

Example · php
<?php
// app/Http/Controllers/Api/PostController.php

declare(strict_types=1);

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Http\Requests\StorePostRequest;
use App\Http\Resources\PostResource;
use App\Models\Post;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;

class PostController extends Controller
{
    public function index(): AnonymousResourceCollection
    {
        // withCount feeds whenCounted; with() feeds whenLoaded.
        $posts = Post::query()
            ->with('author')
            ->withCount('comments')
            ->latest('id')
            ->get();

        return PostResource::collection($posts);
        // Shorthand for the same thing:
        // return $posts->toResourceCollection();
    }

    public function show(Post $post): PostResource
    {
        return new PostResource($post->load('author'));
        // Shorthand: return $post->load('author')->toResource();
    }

    public function store(StorePostRequest $request): JsonResponse
    {
        $post = Post::create($request->validated());

        // 201 and a Location header, rather than a bare 200.
        return (new PostResource($post))
            ->response()
            ->setStatusCode(201)
            ->header('Location', route('posts.show', $post));
    }
}

A resource collection class, for collection-level metadata

Use a collection class when the response needs facts about the whole set rather than about one record. Returning new PostCollection($posts) merges with() into the top level; additional(['meta' => [...]]) does the same thing at the call site when the extra data is specific to one endpoint.

Example · php
<?php
// app/Http/Resources/PostCollection.php
// Generated by: php artisan make:resource PostCollection
//          or: php artisan make:resource Post --collection

declare(strict_types=1);

namespace App\Http\Resources;

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

class PostCollection extends ResourceCollection
{
    /**
     * The resource each item is mapped through.
     *
     * @var string
     */
    public $collects = PostResource::class;

    /**
     * @return array<string, mixed>
     */
    public function toArray(Request $request): array
    {
        return parent::toArray($request);
    }

    /**
     * Top level data, added only when this is the outermost resource.
     *
     * @return array<string, mixed>
     */
    public function with(Request $request): array
    {
        return [
            'meta' => [
                'total_published' => $this->collection
                    ->filter(fn ($post): bool => $post->published_at !== null)
                    ->count(),
            ],
        ];
    }
}

The same transformation, without the framework

A resource is a map callback with a class around it and a data key on top. Seeing it as plain Collection and Str code makes clear why the shape is a decision you make once, in one file, rather than whatever columns happen to exist.

Example · php
<?php

use Illuminate\Support\Str;

// Rows exactly as a query would return them.
$rows = collect([
    ['id' => 1, 'title' => 'Indexes', 'body' => 'An index is a sorted copy of a column.'],
    ['id' => 2, 'title' => 'Locks', 'body' => 'A lock protects a row from two writers.'],
]);

$payload = ['data' => $rows->map(fn (array $row): array => [
    'id' => $row['id'],
    'title' => $row['title'],
    'slug' => Str::slug($row['title']),
    'excerpt' => Str::limit($row['body'], 20),
])->all()];

echo json_encode($payload['data'][1], JSON_PRETTY_PRINT), "\n";

// Output:
// {
//     "id": 2,
//     "title": "Locks",
//     "slug": "locks",
//     "excerpt": "A lock protects a ro..."
// }

Discussion

  • Be the first to comment on this lesson.