Model id: 1 name: "John" email: "j@..." password: *** created_at: ... updated_at: ... Resource toArray() Transform + Filter JSON { "id": 1, "name": "John", "email": "j@.." } Database Transform API Response
Backend

Laravel API Resources: Transforming Data

Mayur Dabhi
Mayur Dabhi
April 2, 2026
18 min read

When building APIs with Laravel, one of the most critical decisions you'll make is how to structure your JSON responses. Directly returning Eloquent models might seem convenient, but it exposes your entire database schema, including sensitive fields like passwords and internal timestamps. This is where Laravel API Resources shine—they provide an elegant transformation layer between your models and the JSON responses sent to your clients.

API Resources give you complete control over how your data is serialized. You can hide sensitive fields, rename attributes, include computed properties, conditionally load relationships, and maintain consistent response structures across your entire API. Whether you're building a mobile app backend, a single-page application API, or a public REST API, mastering API Resources is essential for professional Laravel development.

What You'll Learn
  • Understanding the purpose and architecture of API Resources
  • Creating Resource classes for single models and collections
  • Transforming data with custom attributes and computed values
  • Conditional attributes and relationship loading
  • Working with nested resources and complex relationships
  • Pagination and meta information handling
  • Resource customization with additional data and wrapping
  • Best practices and real-world patterns

Why API Resources Matter

Before API Resources were introduced in Laravel 5.5, developers typically used packages like Fractal or manually created transformation arrays. API Resources provide a native, elegant solution that integrates seamlessly with Laravel's ecosystem. Let's understand why they're crucial for professional API development.

Without vs With API Resources ❌ Without API Resources return User::all(); Exposed Data "id": 1, "name": "John", "email": "...", "password": "***" "remember_token" "email_verified" "created_at" "updated_at" ⚠️ All fields exposed! ✅ With API Resources UserResource ::collection() Resource Transform Filter Format Add Meta JSON "id": 1, "name": "email": ✓ Clean! ✓ Hide sensitive fields ✓ Custom formatting ✓ Consistent structure ✓ Relationship control

API Resources provide a transformation layer that protects sensitive data and gives you complete control over your API responses.

Data Protection

Prevent accidental exposure of sensitive fields like passwords, tokens, and internal IDs.

Consistent Structure

Ensure all API responses follow the same format, making client integration easier.

Decoupled Design

Separate your database schema from your API contract—change one without breaking the other.

Data Transformation

Format dates, compute values, rename fields, and add virtual properties on the fly.

Creating Your First API Resource

Let's start with the basics. We'll create an API Resource for a User model. Laravel provides an Artisan command to generate Resource classes:

Terminal
# Create a single resource
php artisan make:resource UserResource

# Create a resource collection (optional - Laravel auto-generates one)
php artisan make:resource UserCollection --collection

This creates a new file at app/Http/Resources/UserResource.php. Let's examine and customize it:

app/Http/Resources/UserResource.php
<?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 [
            'id' => $this->id,
            'name' => $this->name,
            'email' => $this->email,
            'avatar_url' => $this->avatar 
                ? asset('storage/' . $this->avatar) 
                : null,
            'is_verified' => !is_null($this->email_verified_at),
            'member_since' => $this->created_at->format('F Y'),
            'profile_url' => route('users.show', $this->id),
        ];
    }
}
Pro Tip

Inside a Resource class, $this refers to the underlying model. You can access all model attributes and methods directly, including relationships, accessors, and computed properties.

Using Resources in Controllers

Now let's use our Resource in a controller. There are several ways to return Resources:

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

namespace App\Http\Controllers;

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

class UserController extends Controller
{
    /**
     * Display a single user.
     */
    public function show(User $user)
    {
        // Return a single resource
        return new UserResource($user);
    }

    /**
     * Display all users.
     */
    public function index()
    {
        // Return a collection of resources
        return UserResource::collection(User::all());
    }

    /**
     * Display paginated users.
     */
    public function paginated()
    {
        // Pagination is automatically handled
        return UserResource::collection(User::paginate(15));
    }

    /**
     * Return with additional response data.
     */
    public function withMeta(User $user)
    {
        return (new UserResource($user))
            ->additional([
                'meta' => [
                    'version' => '1.0',
                    'generated_at' => now()->toISOString(),
                ]
            ]);
    }
}

Understanding Resource Architecture

To effectively use API Resources, you need to understand how they work under the hood. Let's visualize the transformation pipeline:

API Resource Transformation Pipeline HTTP Request GET /api/users/1 Controller Fetches Model Eloquent Model User::find(1) API Resource toArray($request) Transform Data JSON Response { "data": {...} } Resource Class Features $this->model Access underlying model attributes $request Access current HTTP request Conditional Data when(), mergeWhen() whenLoaded() Relationships Nested resources Eager loading

Conditional Attributes

One of the most powerful features of API Resources is the ability to conditionally include attributes. This lets you customize responses based on user permissions, request parameters, or loaded relationships.

The when() method includes an attribute only when a condition is true:

public function toArray(Request $request): array
{
    return [
        'id' => $this->id,
        'name' => $this->name,
        'email' => $this->email,
        
        // Only include for authenticated users
        'phone' => $this->when(
            $request->user()?->id === $this->id,
            $this->phone
        ),
        
        // Include admin-only data
        'internal_notes' => $this->when(
            $request->user()?->isAdmin(),
            $this->internal_notes
        ),
        
        // Include based on query parameter
        'detailed_stats' => $this->when(
            $request->has('include_stats'),
            fn() => $this->calculateDetailedStats()
        ),
    ];
}

Use mergeWhen() to conditionally merge multiple attributes:

public function toArray(Request $request): array
{
    return [
        'id' => $this->id,
        'name' => $this->name,
        
        // Merge admin-specific fields
        $this->mergeWhen($request->user()?->isAdmin(), [
            'created_at' => $this->created_at,
            'updated_at' => $this->updated_at,
            'deleted_at' => $this->deleted_at,
            'ip_address' => $this->last_login_ip,
            'login_count' => $this->login_count,
        ]),
        
        // Merge premium user fields
        $this->mergeWhen($this->isPremium(), [
            'subscription_tier' => $this->subscription->tier,
            'features' => $this->getAvailableFeatures(),
        ]),
    ];
}

The whenLoaded() method only includes a relationship if it was eager loaded:

public function toArray(Request $request): array
{
    return [
        'id' => $this->id,
        'name' => $this->name,
        
        // Only include if relationship was loaded
        'posts' => PostResource::collection(
            $this->whenLoaded('posts')
        ),
        
        // With a default value
        'comments_count' => $this->whenLoaded(
            'comments',
            fn() => $this->comments->count(),
            0
        ),
        
        // Nested resource
        'profile' => new ProfileResource(
            $this->whenLoaded('profile')
        ),
        
        // Multiple relationships
        'roles' => $this->whenLoaded('roles', function () {
            return $this->roles->pluck('name');
        }),
    ];
}

// In controller - only eager load when needed:
public function index(Request $request)
{
    $query = User::query();
    
    if ($request->has('with_posts')) {
        $query->with('posts');
    }
    
    return UserResource::collection($query->get());
}
N+1 Query Warning

Always use whenLoaded() for relationships to avoid N+1 queries. If you access a relationship directly without eager loading, each Resource will trigger a separate database query. Use with() in your controller to eager load relationships efficiently.

Nested Resources and Relationships

Real-world APIs often involve complex relationships. API Resources handle nested data elegantly through composition—Resources can contain other Resources:

app/Http/Resources/PostResource.php
<?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,
            'slug' => $this->slug,
            'excerpt' => $this->excerpt,
            'content' => $this->when(
                !$request->is('api/posts'), // Full content on detail view
                $this->content
            ),
            'featured_image' => $this->featured_image 
                ? asset('storage/' . $this->featured_image)
                : null,
            'reading_time' => $this->calculateReadingTime(),
            'published_at' => $this->published_at?->format('M d, Y'),
            'is_featured' => $this->is_featured,
            
            // Nested author resource
            'author' => new UserResource($this->whenLoaded('author')),
            
            // Nested category resource
            'category' => new CategoryResource($this->whenLoaded('category')),
            
            // Collection of nested resources
            'tags' => TagResource::collection($this->whenLoaded('tags')),
            
            // Nested comments with replies (recursive)
            'comments' => CommentResource::collection(
                $this->whenLoaded('comments')
            ),
            
            // Counts (only when loaded)
            'likes_count' => $this->whenCounted('likes'),
            'comments_count' => $this->whenCounted('comments'),
            
            // Links for HATEOAS
            'links' => [
                'self' => route('api.posts.show', $this->id),
                'author' => route('api.users.show', $this->author_id),
            ],
        ];
    }
    
    private function calculateReadingTime(): string
    {
        $words = str_word_count(strip_tags($this->content));
        $minutes = ceil($words / 200);
        return $minutes . ' min read';
    }
}
Nested Resource Structure PostResource "id": 1 "title": "Laravel Tips" "slug": "laravel-tips" UserResource "author": { "id": 5, "name": "John" } CategoryResource "category": { "id": 3, "name": "PHP" } TagResource::collection() "tags": [ { "id": 1, "name": "Laravel" }, { "id": 2, "name": "API" } ] Legend Parent Resource BelongsTo HasMany

Resource Collections

When returning multiple models, you can use either the ::collection() static method or create a dedicated ResourceCollection class for more control:

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

namespace App\Http\Resources;

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

class UserCollection extends ResourceCollection
{
    /**
     * The resource that this resource collects.
     */
    public $collects = UserResource::class;

    /**
     * Transform the resource collection into an array.
     */
    public function toArray(Request $request): array
    {
        return [
            'data' => $this->collection,
            'meta' => [
                'total_users' => $this->collection->count(),
                'verified_users' => $this->collection
                    ->filter(fn($user) => $user->email_verified_at)
                    ->count(),
                'generated_at' => now()->toISOString(),
            ],
        ];
    }
    
    /**
     * Add additional data to the response.
     */
    public function with(Request $request): array
    {
        return [
            'links' => [
                'self' => route('api.users.index'),
            ],
            'api_version' => '2.0',
        ];
    }
}

Pagination with Resources

Laravel seamlessly handles pagination with API Resources. When you pass a paginator to a Resource collection, it automatically includes pagination meta and links:

Controller with Pagination
// In your controller
public function index(Request $request)
{
    $users = User::query()
        ->with(['profile', 'roles'])
        ->when($request->search, function ($query, $search) {
            $query->where('name', 'like', "%{$search}%");
        })
        ->paginate($request->per_page ?? 15);
    
    return UserResource::collection($users);
}

// Response structure:
{
    "data": [
        { "id": 1, "name": "John", ... },
        { "id": 2, "name": "Jane", ... }
    ],
    "links": {
        "first": "http://api.example.com/users?page=1",
        "last": "http://api.example.com/users?page=5",
        "prev": null,
        "next": "http://api.example.com/users?page=2"
    },
    "meta": {
        "current_page": 1,
        "from": 1,
        "last_page": 5,
        "links": [...],
        "path": "http://api.example.com/users",
        "per_page": 15,
        "to": 15,
        "total": 72
    }
}

Advanced Resource Customization

Customizing the Data Wrapper

By default, Resources wrap responses in a "data" key. You can customize or disable this:

Customizing the wrapper
// Disable wrapping for a single resource
class UserResource extends JsonResource
{
    public static $wrap = null; // No wrapper
    // OR
    public static $wrap = 'user'; // Custom wrapper key
}

// Disable wrapping globally in AppServiceProvider
use Illuminate\Http\Resources\Json\JsonResource;

public function boot(): void
{
    JsonResource::withoutWrapping();
}

// Override for specific responses
return (new UserResource($user))->response()->setData([
    'user' => (new UserResource($user))->resolve(),
    'permissions' => $user->permissions,
]);

Response Headers and Status Codes

Customizing HTTP Response
public function store(Request $request)
{
    $user = User::create($request->validated());
    
    return (new UserResource($user))
        ->response()
        ->setStatusCode(201)
        ->header('X-Created-By', $request->user()->id)
        ->header('Location', route('api.users.show', $user));
}

// Or using the withResponse method in the Resource
class UserResource extends JsonResource
{
    public function withResponse(Request $request, JsonResponse $response): void
    {
        if ($this->wasRecentlyCreated) {
            $response->setStatusCode(201);
            $response->header('Location', route('api.users.show', $this->id));
        }
        
        $response->header('X-Resource-Version', '1.0');
    }
}

Best Practices

1

Always Use Resources for API Responses

Never return raw Eloquent models. Create a Resource for every model that appears in your API responses, even if it seems redundant initially.

2

Use whenLoaded() for Relationships

Prevent N+1 queries by always using whenLoaded() for relationships. This ensures relationships are only serialized when explicitly eager loaded.

3

Create Separate Resources for Different Contexts

Consider creating UserResource for public data and UserDetailResource for authenticated user data instead of overloading conditional logic.

4

Use Type Hints and Return Types

Always add proper type hints to your Resource methods. This improves IDE support, catches bugs early, and makes your code self-documenting.

5

Keep Transformations Simple

Resources should transform data, not contain business logic. Complex calculations belong in model methods, accessors, or dedicated service classes.

Real-World Example: E-Commerce API

Let's put everything together with a comprehensive e-commerce example showing multiple interrelated Resources:

ProductResource.php

<?php

namespace App\Http\Resources;

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

class ProductResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'slug' => $this->slug,
            'description' => $this->when(
                $request->routeIs('api.products.show'),
                $this->description
            ),
            
            // Pricing with currency formatting
            'price' => [
                'amount' => $this->price,
                'formatted' => $this->formatted_price,
                'currency' => 'USD',
            ],
            
            // Sale price (conditional)
            $this->mergeWhen($this->isOnSale(), [
                'sale_price' => [
                    'amount' => $this->sale_price,
                    'formatted' => $this->formatted_sale_price,
                    'discount_percent' => $this->discount_percentage,
                ],
            ]),
            
            // Stock information
            'stock' => [
                'available' => $this->stock_quantity,
                'status' => $this->stockStatus(),
                'low_stock_alert' => $this->stock_quantity < 10,
            ],
            
            // Images
            'images' => ImageResource::collection($this->whenLoaded('images')),
            'thumbnail' => $this->thumbnail_url,
            
            // Relationships
            'category' => new CategoryResource($this->whenLoaded('category')),
            'brand' => new BrandResource($this->whenLoaded('brand')),
            'variants' => VariantResource::collection($this->whenLoaded('variants')),
            
            // Reviews summary
            'reviews' => [
                'average_rating' => round($this->reviews_avg_rating, 1),
                'total_count' => $this->reviews_count,
                'breakdown' => $this->when(
                    $request->routeIs('api.products.show'),
                    fn() => $this->getReviewBreakdown()
                ),
            ],
            
            // Admin-only fields
            $this->mergeWhen($request->user()?->isAdmin(), [
                'cost_price' => $this->cost_price,
                'profit_margin' => $this->profit_margin,
                'supplier' => new SupplierResource($this->whenLoaded('supplier')),
            ]),
            
            // Timestamps
            'created_at' => $this->created_at->toISOString(),
            'updated_at' => $this->when(
                $request->user()?->isAdmin(),
                $this->updated_at->toISOString()
            ),
            
            // HATEOAS links
            'links' => [
                'self' => route('api.products.show', $this->slug),
                'add_to_cart' => route('api.cart.add', $this->id),
                'reviews' => route('api.products.reviews', $this->slug),
            ],
        ];
    }
    
    private function stockStatus(): string
    {
        return match(true) {
            $this->stock_quantity === 0 => 'out_of_stock',
            $this->stock_quantity < 10 => 'low_stock',
            default => 'in_stock',
        };
    }
}

OrderResource.php

<?php

namespace App\Http\Resources;

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

class OrderResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'order_number' => $this->order_number,
            'status' => [
                'code' => $this->status,
                'label' => $this->status_label,
                'color' => $this->status_color,
            ],
            
            // Order items
            'items' => OrderItemResource::collection($this->whenLoaded('items')),
            'items_count' => $this->whenCounted('items'),
            
            // Pricing
            'totals' => [
                'subtotal' => $this->subtotal,
                'shipping' => $this->shipping_cost,
                'tax' => $this->tax_amount,
                'discount' => $this->discount_amount,
                'total' => $this->total,
                'currency' => 'USD',
            ],
            
            // Addresses
            'shipping_address' => new AddressResource(
                $this->whenLoaded('shippingAddress')
            ),
            'billing_address' => new AddressResource(
                $this->whenLoaded('billingAddress')
            ),
            
            // Customer (visible to admin)
            'customer' => $this->when(
                $request->user()?->isAdmin(),
                fn() => new UserResource($this->whenLoaded('user'))
            ),
            
            // Tracking
            'shipping' => $this->when($this->shipped_at, [
                'carrier' => $this->shipping_carrier,
                'tracking_number' => $this->tracking_number,
                'tracking_url' => $this->tracking_url,
                'shipped_at' => $this->shipped_at?->toISOString(),
                'estimated_delivery' => $this->estimated_delivery?->format('M d, Y'),
            ]),
            
            // Timestamps
            'placed_at' => $this->created_at->toISOString(),
            'completed_at' => $this->completed_at?->toISOString(),
            
            // Actions
            'can' => [
                'cancel' => $this->canBeCancelled(),
                'return' => $this->canBeReturned(),
                'review' => $this->canLeaveReview(),
            ],
        ];
    }
}

Performance Tips

Practice Why It Matters Implementation
Eager Load Relationships Prevents N+1 queries when serializing collections User::with(['posts', 'profile'])->get()
Use withCount() Get counts without loading full relationships Post::withCount('comments')->get()
Select Specific Columns Reduce memory usage for large datasets User::select(['id', 'name', 'email'])->get()
Lazy Evaluation with Closures Defer expensive operations until needed $this->when($cond, fn() => $expensive)
Cache Computed Values Avoid recalculating on every request Use model accessors with caching

Summary

Laravel API Resources provide a powerful, flexible layer for transforming your Eloquent models into JSON responses. They help you maintain clean separation between your database schema and your API contract, protect sensitive data, and create consistent, well-structured responses.

Key Takeaways

  • Never expose raw models — Always use Resources for API responses
  • Use conditional methodswhen(), mergeWhen(), whenLoaded() for flexible responses
  • Nest Resources — Create a Resource hierarchy that mirrors your model relationships
  • Eager load — Always eager load relationships to prevent N+1 queries
  • Keep it simple — Resources transform data; business logic belongs elsewhere

With API Resources in your toolkit, you're equipped to build professional, scalable APIs that are a joy to consume. Start small with basic Resources, then gradually introduce conditional attributes, nested Resources, and custom collections as your API grows.

Laravel API Resources JSON REST PHP Backend
Mayur Dabhi

Mayur Dabhi

Full-stack developer specializing in Laravel, React, and modern web technologies. Passionate about building scalable applications and sharing knowledge with the developer community.