Laravel API Resources: Transforming Your Eloquent Models for JSON Responses

Published on November 30, 2025
Laravel API JSON Resources Eloquent REST

Introduction to Laravel API Resources

Laravel API Resources provide a transformative layer between your Eloquent models and JSON responses returned to your API consumers. They give you complete control over how your data is presented, including which attributes to include, how to format them, and how to handle relationships.

Why Use API Resources?

  • Complete control over JSON response structure
  • Separation of concerns between models and API responses
  • Consistent response formatting across your API
  • Easy transformation and formatting of data
  • Built-in support for relationships and pagination
  • Versioning and evolution of your API

Common Use Cases:

  • Building RESTful APIs
  • Mobile app backends
  • Single Page Applications (SPAs)
  • Third-party API integrations
  • Microservices communication
  • Public API endpoints

Basic API Resources

Creating Your First Resource

php artisan make:resource UserResource

This creates app/Http/Resources/UserResource.php:

<?php
// app/Http/Resources/UserResource.php

namespace App\Http\Resources;

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

class UserResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     *
     * @return array<string, mixed>
     */
    public function toArray(Request $request): array
    {
        return parent::toArray($request);
    }
}

Basic Resource Transformation

<?php
// app/Http/Resources/UserResource.php

namespace App\Http\Resources;

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

class UserResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     */
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'email' => $this->email,
            'created_at' => $this->created_at,
            'updated_at' => $this->updated_at,
        ];
    }
}

Using Resources in Controllers

<?php
// app/Http/Controllers/UserController.php

namespace App\Http\Controllers;

use App\Http\Resources\UserResource;
use App\Models\User;
use Illuminate\Http\Request;

class UserController extends Controller
{
    public function index()
    {
        $users = User::all();
        
        return UserResource::collection($users);
    }
    
    public function show(User $user)
    {
        return new UserResource($user);
    }
    
    public function store(Request $request)
    {
        $user = User::create($request->validate([
            'name' => 'required|string|max:255',
            'email' => 'required|email|unique:users',
            'password' => 'required|min:8',
        ]));
        
        return new UserResource($user);
    }
}

Advanced Resource Features

Conditional Attributes

<?php
// app/Http/Resources/UserResource.php

namespace App\Http\Resources;

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

class UserResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     */
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'email' => $this->when($request->user() && $request->user()->isAdmin(), $this->email),
            'phone' => $this->when($this->phone_verified_at, $this->phone),
            'profile' => [
                'avatar' => $this->avatar_url,
                'bio' => $this->bio,
                'website' => $this->when($this->website, $this->website),
            ],
            'timestamps' => $this->when($request->user() && $request->user()->isAdmin(), [
                'created_at' => $this->created_at,
                'updated_at' => $this->updated_at,
                'email_verified_at' => $this->email_verified_at,
            ]),
            'stats' => $this->when($request->has('include_stats'), [
                'login_count' => $this->login_count,
                'last_login' => $this->last_login,
            ]),
        ];
    }
}

Data Transformation and Formatting

<?php
// app/Http/Resources/ProductResource.php

namespace App\Http\Resources;

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

class ProductResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     */
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'slug' => $this->slug,
            'description' => $this->description,
            'price' => [
                'amount' => $this->price / 100, // Convert from cents
                'currency' => 'USD',
                'formatted' => '$' . number_format($this->price / 100, 2),
            ],
            'inventory' => [
                'stock' => $this->stock_quantity,
                'status' => $this->stock_quantity > 0 ? 'in_stock' : 'out_of_stock',
                'low_stock' => $this->stock_quantity <= 10,
            ],
            'images' => $this->getImageUrls(),
            'category' => $this->whenLoaded('category', function () {
                return [
                    'id' => $this->category->id,
                    'name' => $this->category->name,
                    'slug' => $this->category->slug,
                ];
            }),
            'metadata' => [
                'weight' => $this->when($this->weight, $this->weight . ' kg'),
                'dimensions' => $this->when($this->dimensions, $this->dimensions),
                'sku' => $this->sku,
            ],
            'timestamps' => [
                'created' => $this->created_at->toISOString(),
                'updated' => $this->updated_at->toISOString(),
                'human_readable' => [
                    'created' => $this->created_at->diffForHumans(),
                    'updated' => $this->updated_at->diffForHumans(),
                ],
            ],
        ];
    }
    
    protected function getImageUrls(): array
    {
        return collect($this->images)->map(function ($image) {
            return [
                'url' => asset("storage/products/{$image}"),
                'thumbnail' => asset("storage/products/thumbnails/{$image}"),
                'alt' => $this->name,
            ];
        })->toArray();
    }
}

Conditional Relationships

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

namespace App\Http\Resources;

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

class PostResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     */
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'title' => $this->title,
            'slug' => $this->slug,
            'content' => $this->when(
                $request->routeIs('posts.show') || $request->user()?->isAdmin(),
                $this->content
            ),
            'excerpt' => $this->excerpt ?? str($this->content)->limit(150),
            'status' => $this->status,
            'featured_image' => $this->featured_image_url,
            
            // Conditional relationships
            'author' => new UserResource($this->whenLoaded('author')),
            'categories' => CategoryResource::collection($this->whenLoaded('categories')),
            'tags' => TagResource::collection($this->whenLoaded('tags')),
            'comments' => $this->when(
                $request->has('include_comments'),
                CommentResource::collection($this->whenLoaded('comments'))
            ),
            'stats' => $this->when(
                $request->user()?->isAdmin(),
                [
                    'views' => $this->views,
                    'likes' => $this->likes_count,
                    'shares' => $this->shares_count,
                ]
            ),
            
            'meta' => [
                'url' => route('posts.show', $this->slug),
                'edit_url' => $this->when($request->user()?->can('update', $this), 
                    route('admin.posts.edit', $this)
                ),
            ],
            
            'timestamps' => [
                'published_at' => $this->published_at?->toISOString(),
                'created_at' => $this->created_at->toISOString(),
                'updated_at' => $this->updated_at->toISOString(),
            ],
        ];
    }
}

Working with Relationships

Eager Loading in Resources

<?php
// app/Http/Controllers/PostController.php

namespace App\Http\Controllers;

use App\Http\Resources\PostResource;
use App\Models\Post;
use Illuminate\Http\Request;

class PostController extends Controller
{
    public function index(Request $request)
    {
        $query = Post::with(['author', 'categories', 'tags']);
        
        if ($request->has('include_comments')) {
            $query->with('comments');
        }
        
        $posts = $query->latest()->paginate(10);
        
        return PostResource::collection($posts);
    }
    
    public function show(Request $request, Post $post)
    {
        $post->load(['author', 'categories', 'tags', 'comments.user']);
        
        return new PostResource($post);
    }
}

Nested Resources

<?php
// app/Http/Resources/OrderResource.php

namespace App\Http\Resources;

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

class OrderResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     */
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'order_number' => $this->order_number,
            'status' => $this->status,
            'total' => [
                'amount' => $this->total_amount / 100,
                'currency' => $this->currency,
                'formatted' => $this->formatted_total,
            ],
            
            // Customer information
            'customer' => new UserResource($this->whenLoaded('customer')),
            
            // Shipping information
            'shipping_address' => $this->whenLoaded('shippingAddress', function () {
                return [
                    'name' => $this->shippingAddress->full_name,
                    'street' => $this->shippingAddress->address_line_1,
                    'city' => $this->shippingAddress->city,
                    'state' => $this->shippingAddress->state,
                    'postal_code' => $this->shippingAddress->postal_code,
                    'country' => $this->shippingAddress->country,
                ];
            }),
            
            // Order items with products
            'items' => OrderItemResource::collection($this->whenLoaded('items')),
            
            // Payment information
            'payment' => $this->whenLoaded('payment', function () {
                return [
                    'method' => $this->payment->method,
                    'status' => $this->payment->status,
                    'transaction_id' => $this->payment->transaction_id,
                    'paid_at' => $this->payment->paid_at?->toISOString(),
                ];
            }),
            
            // Timestamps
            'dates' => [
                'placed_at' => $this->created_at->toISOString(),
                'updated_at' => $this->updated_at->toISOString(),
                'estimated_delivery' => $this->estimated_delivery_date?->toISOString(),
            ],
            
            // Actions (conditional based on user permissions)
            'actions' => $this->when($request->user(), function () use ($request) {
                return [
                    'can_cancel' => $request->user()->can('cancel', $this),
                    'can_return' => $request->user()->can('return', $this),
                    'can_view_invoice' => $request->user()->can('viewInvoice', $this),
                ];
            }),
        ];
    }
}
<?php
// app/Http/Resources/OrderItemResource.php

namespace App\Http\Resources;

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

class OrderItemResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     */
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'product' => new ProductResource($this->whenLoaded('product')),
            'product_name' => $this->product_name, // In case product is deleted
            'quantity' => $this->quantity,
            'unit_price' => [
                'amount' => $this->unit_price / 100,
                'formatted' => '$' . number_format($this->unit_price / 100, 2),
            ],
            'total_price' => [
                'amount' => $this->total_price / 100,
                'formatted' => '$' . number_format($this->total_price / 100, 2),
            ],
            'variants' => $this->when($this->variants, function () {
                return json_decode($this->variants, true);
            }),
        ];
    }
}

Collection Resources

Custom Collection Resources

php artisan make:resource UserCollection
<?php
// app/Http/Resources/UserCollection.php

namespace App\Http\Resources;

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

class UserCollection extends ResourceCollection
{
    /**
     * Transform the resource collection into an array.
     *
     * @return array<int|string, mixed>
     */
    public function toArray(Request $request): array
    {
        return [
            'data' => $this->collection,
            'meta' => [
                'current_page' => $this->currentPage(),
                'last_page' => $this->lastPage(),
                'per_page' => $this->perPage(),
                'total' => $this->total(),
                'from' => $this->firstItem(),
                'to' => $this->lastItem(),
            ],
            'links' => [
                'first' => $this->url(1),
                'last' => $this->url($this->lastPage()),
                'prev' => $this->previousPageUrl(),
                'next' => $this->nextPageUrl(),
            ],
        ];
    }
    
    /**
     * Customize the response for the resource.
     */
    public function withResponse($request, $response)
    {
        $response->header('X-API-Version', '1.0');
        $response->header('X-API-Environment', config('app.env'));
    }
}

Advanced Collection Resources

<?php
// app/Http/Resources/ProductCollection.php

namespace App\Http\Resources;

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

class ProductCollection extends ResourceCollection
{
    /**
     * Transform the resource collection into an array.
     */
    public function toArray(Request $request): array
    {
        return [
            'data' => ProductResource::collection($this->collection),
            'meta' => $this->getMeta(),
            'filters' => $this->getFilters($request),
            'links' => $this->getLinks(),
        ];
    }
    
    protected function getMeta(): array
    {
        return [
            'pagination' => [
                'current_page' => $this->currentPage(),
                'last_page' => $this->lastPage(),
                'per_page' => $this->perPage(),
                'total' => $this->total(),
                'from' => $this->firstItem(),
                'to' => $this->lastItem(),
            ],
            'sorting' => [
                'field' => request('sort', 'created_at'),
                'direction' => request('direction', 'desc'),
            ],
        ];
    }
    
    protected function getFilters(Request $request): array
    {
        return [
            'applied' => [
                'category' => $request->category,
                'price_min' => $request->price_min,
                'price_max' => $request->price_max,
                'search' => $request->search,
            ],
            'available' => [
                'categories' => \App\Models\Category::all()->pluck('name', 'id'),
                'price_ranges' => [
                    '0-25' => 'Under $25',
                    '25-50' => '$25 to $50',
                    '50-100' => '$50 to $100',
                    '100-9999' => 'Over $100',
                ],
            ],
        ];
    }
    
    protected function getLinks(): array
    {
        return [
            'self' => $this->url($this->currentPage()),
            'first' => $this->url(1),
            'last' => $this->url($this->lastPage()),
            'prev' => $this->previousPageUrl(),
            'next' => $this->nextPageUrl(),
        ];
    }
    
    /**
     * Customize the outgoing response for the resource.
     */
    public function withResponse($request, $response)
    {
        $response->header('Content-Type', 'application/json');
        $response->header('X-API-Version', '1.0');
        $response->header('X-Total-Count', $this->total());
    }
}

Pagination with Resources

Basic Pagination

<?php
// app/Http/Controllers/ProductController.php

namespace App\Http\Controllers;

use App\Http\Resources\ProductCollection;
use App\Http\Resources\ProductResource;
use App\Models\Product;
use Illuminate\Http\Request;

class ProductController extends Controller
{
    public function index(Request $request)
    {
        $query = Product::with(['category', 'brand']);
        
        // Search
        if ($request->has('search')) {
            $query->where('name', 'like', '%' . $request->search . '%');
        }
        
        // Filter by category
        if ($request->has('category')) {
            $query->where('category_id', $request->category);
        }
        
        // Price range
        if ($request->has('price_min')) {
            $query->where('price', '>=', $request->price_min * 100);
        }
        
        if ($request->has('price_max')) {
            $query->where('price', '<=', $request->price_max * 100);
        }
        
        // Sorting
        $sortField = $request->get('sort', 'created_at');
        $sortDirection = $request->get('direction', 'desc');
        $query->orderBy($sortField, $sortDirection);
        
        $products = $query->paginate($request->get('per_page', 15));
        
        return new ProductCollection($products);
    }
}

Custom Pagination Responses

<?php
// app/Http/Resources/PaginatedResource.php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\ResourceCollection;

abstract class PaginatedResource extends ResourceCollection
{
    public function toArray($request)
    {
        return [
            'data' => $this->getResourceClass()::collection($this->collection),
            'meta' => $this->meta(),
            'links' => $this->links(),
        ];
    }
    
    abstract protected function getResourceClass(): string;
    
    protected function meta(): array
    {
        return [
            'current_page' => $this->currentPage(),
            'from' => $this->firstItem(),
            'last_page' => $this->lastPage(),
            'links' => $this->getPaginatedLinks(),
            'path' => $this->path(),
            'per_page' => $this->perPage(),
            'to' => $this->lastItem(),
            'total' => $this->total(),
        ];
    }
    
    protected function links(): array
    {
        return [
            'first' => $this->url(1),
            'last' => $this->url($this->lastPage()),
            'prev' => $this->previousPageUrl(),
            'next' => $this->nextPageUrl(),
        ];
    }
    
    protected function getPaginatedLinks(): array
    {
        $links = [];
        $currentPage = $this->currentPage();
        $lastPage = $this->lastPage();
        
        // Show up to 5 pages around current page
        $start = max(1, $currentPage - 2);
        $end = min($lastPage, $currentPage + 2);
        
        for ($page = $start; $page <= $end; $page++) {
            $links[] = [
                'url' => $this->url($page),
                'label' => $page,
                'active' => $page === $currentPage,
            ];
        }
        
        return $links;
    }
}

// Usage for specific resources
class PaginatedProductResource extends PaginatedResource
{
    protected function getResourceClass(): string
    {
        return ProductResource::class;
    }
}

Real-World API Examples

E-commerce API Resources

<?php
// app/Http/Resources/CartResource.php

namespace App\Http\Resources;

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

class CartResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     */
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'user_id' => $this->user_id,
            'session_id' => $this->session_id,
            'items' => CartItemResource::collection($this->whenLoaded('items')),
            'summary' => [
                'items_count' => $this->items_count,
                'unique_products' => $this->unique_products_count,
                'subtotal' => [
                    'amount' => $this->subtotal / 100,
                    'formatted' => '$' . number_format($this->subtotal / 100, 2),
                ],
                'tax' => [
                    'amount' => $this->tax_amount / 100,
                    'formatted' => '$' . number_format($this->tax_amount / 100, 2),
                    'rate' => $this->tax_rate,
                ],
                'shipping' => [
                    'amount' => $this->shipping_cost / 100,
                    'formatted' => '$' . number_format($this->shipping_cost / 100, 2),
                    'method' => $this->shipping_method,
                ],
                'total' => [
                    'amount' => $this->total_amount / 100,
                    'formatted' => '$' . number_format($this->total_amount / 100, 2),
                ],
            ],
            'coupon' => $this->whenLoaded('coupon', function () {
                return [
                    'code' => $this->coupon->code,
                    'discount' => [
                        'amount' => $this->coupon_discount / 100,
                        'formatted' => '$' . number_format($this->coupon_discount / 100, 2),
                        'type' => $this->coupon->type,
                    ],
                ];
            }),
            'timestamps' => [
                'created_at' => $this->created_at->toISOString(),
                'updated_at' => $this->updated_at->toISOString(),
                'abandoned_after' => $this->created_at->addHours(24)->diffForHumans(),
            ],
        ];
    }
}
<?php
// app/Http/Resources/CartItemResource.php

namespace App\Http\Resources;

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

class CartItemResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     */
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'cart_id' => $this->cart_id,
            'product' => new ProductResource($this->whenLoaded('product')),
            'quantity' => $this->quantity,
            'unit_price' => [
                'amount' => $this->unit_price / 100,
                'formatted' => '$' . number_format($this->unit_price / 100, 2),
            ],
            'total_price' => [
                'amount' => $this->total_price / 100,
                'formatted' => '$' . number_format($this->total_price / 100, 2),
            ],
            'customizations' => $this->when($this->customizations, function () {
                return json_decode($this->customizations, true);
            }),
            'notes' => $this->notes,
            'added_at' => $this->created_at->toISOString(),
        ];
    }
}

Blog API Resources

<?php
// app/Http/Resources/BlogPostResource.php

namespace App\Http\Resources;

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

class BlogPostResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     */
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'title' => $this->title,
            'slug' => $this->slug,
            'excerpt' => $this->excerpt,
            'content' => $this->when(
                $this->shouldIncludeContent($request),
                $this->content
            ),
            'featured_image' => $this->getFeaturedImage(),
            'status' => $this->status,
            'reading_time' => $this->reading_time . ' min read',
            
            // Author information
            'author' => new UserResource($this->whenLoaded('author')),
            
            // Categories and tags
            'categories' => CategoryResource::collection($this->whenLoaded('categories')),
            'tags' => TagResource::collection($this->whenLoaded('tags')),
            
            // Comments (with pagination)
            'comments' => $this->when(
                $request->has('include_comments'),
                function () use ($request) {
                    $comments = $this->comments()
                        ->with('user')
                        ->where('approved', true)
                        ->latest()
                        ->paginate(10);
                    
                    return CommentResource::collection($comments);
                }
            ),
            
            // SEO metadata
            'seo' => [
                'meta_title' => $this->meta_title,
                'meta_description' => $this->meta_description,
                'canonical_url' => $this->canonical_url,
                'og_image' => $this->og_image_url,
            ],
            
            // Engagement metrics
            'engagement' => $this->when(
                $request->user()?->can('view_analytics', $this),
                [
                    'views' => $this->views,
                    'likes' => $this->likes_count,
                    'shares' => $this->shares_count,
                    'comments_count' => $this->comments_count,
                ]
            ),
            
            // Timestamps
            'dates' => [
                'published_at' => $this->published_at?->toISOString(),
                'created_at' => $this->created_at->toISOString(),
                'updated_at' => $this->updated_at->toISOString(),
            ],
            
            // URLs
            'urls' => [
                'public' => route('blog.show', $this->slug),
                'api' => route('api.posts.show', $this->id),
                'edit' => $this->when($request->user()?->can('update', $this), 
                    route('admin.posts.edit', $this)
                ),
            ],
        ];
    }
    
    protected function shouldIncludeContent(Request $request): bool
    {
        return $request->routeIs('api.posts.show') || 
               $request->user()?->can('view_draft', $this);
    }
    
    protected function getFeaturedImage(): ?array
    {
        if (!$this->featured_image) {
            return null;
        }
        
        return [
            'url' => asset("storage/posts/{$this->featured_image}"),
            'alt' => $this->title,
            'caption' => $this->featured_image_caption,
            'sizes' => [
                'thumbnail' => asset("storage/posts/thumbnails/{$this->featured_image}"),
                'medium' => asset("storage/posts/medium/{$this->featured_image}"),
                'large' => asset("storage/posts/large/{$this->featured_image}"),
            ],
        ];
    }
}

Advanced Resource Patterns

Resource Caching

<?php
// app/Http/Resources/CachedResource.php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Facades\Cache;

abstract class CachedResource extends JsonResource
{
    protected $cacheKey;
    protected $cacheTtl = 3600; // 1 hour
    
    public function toArray($request)
    {
        $this->cacheKey = $this->getCacheKey($request);
        
        return Cache::remember($this->cacheKey, $this->cacheTtl, function () use ($request) {
            return $this->getData($request);
        });
    }
    
    abstract protected function getData($request): array;
    
    abstract protected function getCacheKey($request): string;
    
    /**
     * Clear the cached resource
     */
    public static function clearCache($model): void
    {
        $resource = new static($model);
        Cache::forget($resource->getCacheKey(request()));
    }
}

// Usage
class CachedProductResource extends CachedResource
{
    protected function getData($request): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'price' => $this->price / 100,
            // ... other fields
        ];
    }
    
    protected function getCacheKey($request): string
    {
        return "product:{$this->id}:v2:" . md5(serialize($request->all()));
    }
}

API Versioning with Resources

<?php
// app/Http/Resources/v1/UserResource.php

namespace App\Http\Resources\v1;

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

class UserResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'email' => $this->email,
            'created_at' => $this->created_at->toISOString(),
        ];
    }
}
<?php
// app/Http/Resources/v2/UserResource.php

namespace App\Http\Resources\v2;

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

class UserResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'type' => 'users',
            'id' => $this->id,
            'attributes' => [
                'name' => $this->name,
                'email' => $this->email,
                'profile' => [
                    'avatar' => $this->avatar_url,
                    'bio' => $this->bio,
                ],
            ],
            'relationships' => [
                'posts' => [
                    'links' => [
                        'related' => route('api.v2.users.posts', $this->id),
                    ],
                ],
            ],
            'links' => [
                'self' => route('api.v2.users.show', $this->id),
            ],
            'meta' => [
                'created_at' => $this->created_at->toISOString(),
                'updated_at' => $this->updated_at->toISOString(),
            ],
        ];
    }
}

Resource Response Customization

<?php
// app/Http/Resources/ApiResource.php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

abstract class ApiResource extends JsonResource
{
    /**
     * Customize the response for the resource.
     */
    public function withResponse($request, $response)
    {
        $response->header('X-API-Version', $this->getApiVersion());
        $response->header('X-API-Environment', config('app.env'));
        $response->header('X-Content-Type-Options', 'nosniff');
        
        // Add CORS headers if needed
        if (config('app.env') === 'local') {
            $response->header('Access-Control-Allow-Origin', '*');
        }
    }
    
    /**
     * Get additional data that should be returned with the resource array.
     */
    public function with($request)
    {
        return [
            'meta' => [
                'version' => $this->getApiVersion(),
                'timestamp' => now()->toISOString(),
                'copyright' => '© ' . date('Y') . ' Your Company',
            ],
            'links' => [
                'documentation' => 'https://api.example.com/docs',
            ],
        ];
    }
    
    abstract protected function getApiVersion(): string;
}

Testing API Resources

Resource Testing

<?php
// tests/Unit/UserResourceTest.php

namespace Tests\Unit;

use Tests\TestCase;
use App\Http\Resources\UserResource;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Foundation\Testing\RefreshDatabase;

class UserResourceTest extends TestCase
{
    use RefreshDatabase;

    public function test_user_resource_returns_correct_structure()
    {
        $user = User::factory()->create([
            'name' => 'John Doe',
            'email' => 'john@example.com',
        ]);
        
        $resource = new UserResource($user);
        $request = Request::create('/');
        
        $response = $resource->toArray($request);
        
        $this->assertEquals([
            'id' => $user->id,
            'name' => 'John Doe',
            'email' => 'john@example.com',
            'created_at' => $user->created_at->toISOString(),
            'updated_at' => $user->updated_at->toISOString(),
        ], $response);
    }
    
    public function test_user_resource_hides_email_for_non_admins()
    {
        $user = User::factory()->create();
        $nonAdmin = User::factory()->create(['is_admin' => false]);
        
        $request = Request::create('/');
        $request->setUserResolver(function () use ($nonAdmin) {
            return $nonAdmin;
        });
        
        $resource = new UserResource($user);
        $response = $resource->toArray($request);
        
        $this->assertArrayNotHasKey('email', $response);
    }
    
    public function test_user_resource_shows_email_for_admins()
    {
        $user = User::factory()->create();
        $admin = User::factory()->create(['is_admin' => true]);
        
        $request = Request::create('/');
        $request->setUserResolver(function () use ($admin) {
            return $admin;
        });
        
        $resource = new UserResource($user);
        $response = $resource->toArray($request);
        
        $this->assertArrayHasKey('email', $response);
        $this->assertEquals($user->email, $response['email']);
    }
}

Collection Resource Testing

<?php
// tests/Unit/UserCollectionTest.php

namespace Tests\Unit;

use Tests\TestCase;
use App\Http\Resources\UserCollection;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Pagination\LengthAwarePaginator;

class UserCollectionTest extends TestCase
{
    use RefreshDatabase;

    public function test_user_collection_returns_paginated_structure()
    {
        User::factory()->count(15)->create();
        
        $paginator = User::paginate(10);
        $collection = new UserCollection($paginator);
        
        $response = $collection->toArray(request());
        
        $this->assertArrayHasKey('data', $response);
        $this->assertArrayHasKey('meta', $response);
        $this->assertArrayHasKey('links', $response);
        
        $this->assertCount(10, $response['data']);
        $this->assertEquals(15, $response['meta']['total']);
        $this->assertEquals(2, $response['meta']['last_page']);
    }
    
    public function test_user_collection_includes_custom_headers()
    {
        User::factory()->count(5)->create();
        
        $paginator = User::paginate(10);
        $collection = new UserCollection($paginator);
        
        $request = request();
        $response = response()->json($collection->toArray($request));
        
        $collection->withResponse($request, $response);
        
        $this->assertEquals('1.0', $response->headers->get('X-API-Version'));
        $this->assertEquals(config('app.env'), $response->headers->get('X-API-Environment'));
    }
}

Performance Optimization

Eager Loading Optimization

<?php
// app/Http/Controllers/PostController.php

namespace App\Http\Controllers;

use App\Http\Resources\PostResource;
use App\Models\Post;
use Illuminate\Http\Request;

class PostController extends Controller
{
    public function index(Request $request)
    {
        $query = Post::with([
            'author' => function ($query) {
                $query->select('id', 'name', 'avatar_url');
            },
            'categories' => function ($query) {
                $query->select('id', 'name', 'slug');
            },
            'tags' => function ($query) {
                $query->select('id', 'name', 'slug');
            },
        ]);
        
        // Only load comments if explicitly requested
        if ($request->has('include_comments')) {
            $query->with(['comments' => function ($query) {
                $query->with(['user' => function ($query) {
                    $query->select('id', 'name');
                }])->where('approved', true);
            }]);
        }
        
        $posts = $query->latest()->paginate(15);
        
        return PostResource::collection($posts);
    }
}

Selective Field Loading

<?php
// app/Http/Resources/OptimizedUserResource.php

namespace App\Http\Resources;

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

class OptimizedUserResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     */
    public function toArray(Request $request): array
    {
        // Only load specific fields based on request
        $fields = $request->get('fields', 'default');
        
        return match($fields) {
            'minimal' => $this->getMinimalFields(),
            'profile' => $this->getProfileFields(),
            'admin' => $this->getAdminFields(),
            default => $this->getDefaultFields(),
        };
    }
    
    protected function getMinimalFields(): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'avatar' => $this->avatar_url,
        ];
    }
    
    protected function getDefaultFields(): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'email' => $this->when($request->user()?->isAdmin(), $this->email),
            'avatar' => $this->avatar_url,
            'bio' => $this->bio,
            'joined_at' => $this->created_at->diffForHumans(),
        ];
    }
    
    protected function getProfileFields(): array
    {
        return [
            ...$this->getDefaultFields(),
            'website' => $this->website,
            'location' => $this->location,
            'social_links' => $this->social_links,
        ];
    }
    
    protected function getAdminFields(): array
    {
        return [
            ...$this->getProfileFields(),
            'email_verified_at' => $this->email_verified_at,
            'last_login_at' => $this->last_login_at,
            'login_count' => $this->login_count,
            'is_admin' => $this->is_admin,
        ];
    }
}

Common Interview Questions & Answers

1. What are Laravel API Resources?
API Resources provide a transformation layer that sits between your Eloquent models and the JSON responses returned by your API. They allow you to customize and control exactly how your data is presented.

2. What's the difference between JsonResource and ResourceCollection?
JsonResource is for transforming a single model instance, while ResourceCollection is for transforming a collection of models (including paginated results).

3. How do you handle relationships in API Resources?
Use conditional loading with whenLoaded() method and nest other resources within your main resource to include related data.

4. What's the purpose of the when() method?
The when() method conditionally includes attributes in the response based on a given condition, helping you create dynamic responses.

5. How do you customize pagination responses?
Create a custom ResourceCollection class and override the toArray() method to customize the pagination structure.

6. How can you improve performance with API Resources?
Use eager loading to prevent N+1 queries, implement selective field loading, and consider caching for frequently accessed resources.

7. What's the difference between make:hidden() and conditional attributes?
make:hidden() permanently hides attributes from the model, while conditional attributes in resources give you dynamic control over what's included in the API response.

8. How do you handle API versioning with resources?
Create versioned resource classes in separate directories (like v1/, v2/) and use the appropriate version in your controllers based on the API version.

9. How can you test API Resources?
Create unit tests that instantiate resources with model data and assert the transformed output matches your expectations.

10. What are some best practices for API Resources?
Keep transformation logic in resources, not controllers
Use consistent response structures
Implement proper error handling
Document your API responses
Consider performance implications
Use meaningful status codes and messages

You've now mastered Laravel API Resources! From basic transformations to advanced patterns like caching, versioning, and performance optimization, you have the complete toolkit for building robust, scalable APIs with clean, consistent JSON responses.