API Resources
๐ฆ API Resources
ModelSerializer does double duty โ validating incoming data and serializing outgoing data, in one class. Laravel splits these into two: FormRequest (Course 1, Chapter 7) validates input; API Resources handle output only. Neither replaces the other.
Creating a Resource
php artisan make:resource BookResource
// app/Http/Resources/BookResource.php namespace App\Http\Resources; use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\JsonResource; class BookResource extends JsonResource { public function toArray(Request $request): array { return [ 'id' => $this->id, 'title' => $this->title, 'price' => $this->price, ]; } }
// app/Http/Controllers/BookController.php use App\Http\Resources\BookResource; public function show(Book $book) { return new BookResource($book); } public function index() { return BookResource::collection(Book::all()); }
DRF's One Class vs Laravel's Two-Class Split
DRF: One Serializer, Both Directions
class BookSerializer(serializers.ModelSerializer): class Meta: model = Book fields = ["id", "title", "price"] # Same class validates incoming data AND serializes outgoing data.
Laravel: Two Classes, One Direction Each
// StoreBookRequest (Ch.7) โ validates INPUT only // BookResource (this chapter) โ shapes OUTPUT only // Neither class knows about the other.
๐ Conditional Attributes
whenLoaded() is a genuinely Resource-specific feature โ it includes a relationship only if it was already eager-loaded, and never triggers a query of its own:
class BookResource extends JsonResource { public function toArray(Request $request): array { return [ 'id' => $this->id, 'title' => $this->title, 'author' => new AuthorResource($this->whenLoaded('author')), 'is_premium' => $this->when($this->price > 50, true), ]; } }
whenLoaded('author')
Includes the author key only if with('author') was called upstream โ omits it entirely (not null) if the relationship was never eager loaded.
when($condition, $value)
Includes a field only if a condition is true โ useful for fields that only make sense in certain states (e.g. an admin-only field, or a computed flag).
Nesting Resources
// app/Http/Resources/AuthorResource.php class AuthorResource extends JsonResource { public function toArray(Request $request): array { return [ 'id' => $this->id, 'name' => $this->name, ]; } }
Nesting AuthorResource inside BookResource's toArray() (as shown above) produces a JSON payload with the author embedded as a sub-object โ the same nested-shape idea as a DRF serializer with a nested serializer field.
Resource Collections & Pagination
public function index() { return BookResource::collection(Book::paginate(20)); } // Response automatically includes "data", "links", and "meta" // (current_page, total, per_page, ...) โ no manual pagination wiring needed.
โ ๏ธ Resources Don't Prevent N+1 โ They Just Format It
A critical, easy-to-miss trap: a Resource transforms whatever data is on the model โ it does nothing to prevent the N+1 problem from Chapter 6 if the underlying query never eager-loaded the relationship in the first place:
N+1 Through a Resource vs Eager-Loaded First
Still N+1
public function index() { return BookResource::collection(Book::all()); // If BookResource::toArray() accesses $this->author, // that's still 1 query PER book โ the Resource doesn't know or care. }
Fixed at the Query, Not the Resource
public function index() { return BookResource::collection( Book::with('author')->get() ); // Eager loading happens BEFORE the Resource ever runs. }
whenLoaded() is the safety net for this specific trap โ using it instead of a direct $this->author access means a forgotten with('author') silently omits the field rather than silently triggering N+1 queries.
๐ป Coding Challenges
Challenge 1: Write a Basic Resource
Write an AuthorResource exposing id, name, and bio, and a controller index() method returning AuthorResource::collection(Author::all()).
Goal: Practice the basic toArray() shape and collection usage.
Challenge 2: Nest a Resource With whenLoaded()
Extend BookResource to include a nested PublisherResource using whenLoaded('publisher'), and demonstrate the difference in output between a query that eager-loads publisher and one that doesn't.
Goal: Practice the safe, N+1-avoiding pattern for including a relationship.
Challenge 3: Fix an N+1 Hiding Behind a Resource
Given a controller index() method returning BookResource::collection(Book::all()) where BookResource::toArray() directly accesses $this->author->name, identify the N+1 problem and fix it โ without changing BookResource itself.
Goal: Practice recognizing that the fix belongs at the query level, not inside the Resource.
It's tempting to assume a nicely encapsulated Resource class handles performance concerns the way it handles shape concerns โ it doesn't. $this->author->name inside a toArray() method triggers a real Eloquent lazy load if author wasn't eager-loaded by the query that produced the model in the first place, exactly the same N+1 trap from Chapter 6, just one layer removed and easier to miss because the Resource class itself looks completely correct in isolation. Always check the query feeding a Resource collection, not just the Resource's own code, when investigating N+1 issues on an API endpoint.
๐ฏ What's Next
With a real API taking shape, the next chapter covers proving it works: Testing Laravel Apps โ PHPUnit, feature tests, and factories for generating test data.