Laravel Framework

API Resources & Transformations

18 min Lesson 22 of 45

API Resources & Transformations

When building APIs, you need to control exactly what data is exposed and how it's formatted. Laravel's API resources provide a transformation layer between your Eloquent models and the JSON responses sent to clients. This lesson covers how to create flexible, maintainable API responses.

Understanding API Resources

API Resources act as a transformation layer that sits between your models and your API responses. They allow you to:

  • Control which model attributes are exposed
  • Transform data formats (dates, numbers, etc.)
  • Include or exclude related data conditionally
  • Add computed attributes
  • Maintain consistent API response structures
Creating a Resource:
// Generate a resource class
php artisan make:resource UserResource

// 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 [
            'id' => $this->id,
            'name' => $this->name,
            'email' => $this->email,
            'created_at' => $this->created_at->toDateTimeString(),
            'updated_at' => $this->updated_at->toDateTimeString(),
        ];
    }
}

// Using in controller
use App\Http\Resources\UserResource;
use App\Models\User;

public function show($id)
{
    $user = User::findOrFail($id);
    return new UserResource($user);
}
Important: Within a resource class, $this refers to the underlying model instance. You can access all model properties and methods using $this->propertyName.

Resource Collections

When returning multiple resources, Laravel provides collection resources to handle arrays of data:

Creating and Using Collections:
// Generate a collection resource
php artisan make:resource UserCollection

// 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' => [
                'total' => $this->collection->count(),
                'generated_at' => now()->toIso8601String(),
            ],
        ];
    }
}

// Using in controller
public function index()
{
    $users = User::all();
    return new UserCollection($users);
}

// Or use the collection() method on a single resource
public function index()
{
    return UserResource::collection(User::all());
}
Quick Tip: For simple collections, use ResourceClass::collection() instead of creating a separate collection class. Reserve collection classes for when you need custom meta data or wrapper modifications.

Conditional Attributes

Resources can include or exclude attributes based on conditions, providing flexibility in your API responses:

Conditional Inclusion:
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,
            'content' => $this->content,
            'author' => $this->author->name,

            // Include only when condition is true
            'edit_url' => $this->when(
                $request->user()?->can('update', $this->resource),
                route('posts.edit', $this->id)
            ),

            // Include with default value
            'views' => $this->when($this->views !== null, $this->views, 0),

            // Merge attributes conditionally
            $this->mergeWhen($request->user()?->isAdmin(), [
                'internal_notes' => $this->notes,
                'flagged' => $this->is_flagged,
            ]),

            // Include related resource conditionally
            'comments' => CommentResource::collection(
                $this->whenLoaded('comments')
            ),

            'category' => new CategoryResource(
                $this->whenLoaded('category')
            ),

            // Include pivot data if exists
            'pivot' => $this->whenPivotLoaded('post_tag', function () {
                return [
                    'created_at' => $this->pivot->created_at,
                ];
            }),

            'created_at' => $this->created_at->toDateTimeString(),
            'updated_at' => $this->updated_at->toDateTimeString(),
        ];
    }
}
N+1 Query Warning: Always use whenLoaded() for relationships to avoid N+1 query problems. It only includes the relationship if it was eager loaded with with().

Adding Meta Data and Links

Resources can include additional meta data and hypermedia links to make your API more informative:

Meta Data and Links:
namespace App\Http\Resources;

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,
        ];
    }

    /**
     * Add additional data to the resource response.
     *
     * @return array<string, mixed>
     */
    public function with(Request $request): array
    {
        return [
            'meta' => [
                'version' => '1.0',
                'generated_at' => now()->toIso8601String(),
            ],
        ];
    }

    /**
     * Add links to the resource response.
     *
     * @return array<string, string>
     */
    public function withResponse(Request $request, $response): void
    {
        $response->header('X-Resource-Version', '1.0');
    }
}

// Custom wrapper key
class UserResource extends JsonResource
{
    public static $wrap = 'user';  // Default is 'data'

    // Or disable wrapping entirely
    public static $wrap = null;
}

// Response will look like:
{
    "user": {
        "id": 1,
        "name": "John Doe",
        "email": "john@example.com"
    },
    "meta": {
        "version": "1.0",
        "generated_at": "2026-02-14T10:30:00Z"
    }
}

Pagination with Resources

Resources work seamlessly with Laravel's pagination, automatically including pagination meta data:

Paginated Resources:
// Controller
public function index()
{
    $users = User::paginate(15);
    return UserResource::collection($users);
}

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

// Customize pagination meta in collection
class UserCollection extends ResourceCollection
{
    public function toArray(Request $request): array
    {
        return [
            'data' => $this->collection,
            'pagination' => [
                'total' => $this->total(),
                'count' => $this->count(),
                'per_page' => $this->perPage(),
                'current_page' => $this->currentPage(),
                'total_pages' => $this->lastPage(),
            ],
        ];
    }
}

Nested Resources and Relationships

Resources can contain other resources, allowing you to build complex nested structures:

Nested Resource Structures:
// UserResource with nested resources
namespace App\Http\Resources;

class UserResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'email' => $this->email,

            // Nested resource for single relationship
            'profile' => new ProfileResource($this->whenLoaded('profile')),

            // Nested collection for multiple relationships
            'posts' => PostResource::collection($this->whenLoaded('posts')),

            // Conditional nested resource
            'company' => $this->when(
                $this->relationLoaded('company') && $this->company,
                new CompanyResource($this->company)
            ),
        ];
    }
}

// Controller with eager loading
public function show($id)
{
    $user = User::with(['profile', 'posts', 'company'])->findOrFail($id);
    return new UserResource($user);
}
Performance Tip: Always eager load relationships that you'll include in resources. Use whenLoaded() to conditionally include them only when they're present, avoiding N+1 queries.

Computed and Custom Attributes

Resources can add computed values that don't exist in the database:

Adding Computed Attributes:
namespace App\Http\Resources;

class ProductResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'price' => $this->price,
            'sale_price' => $this->sale_price,

            // Computed attributes
            'discount_percentage' => $this->calculateDiscountPercentage(),
            'is_on_sale' => $this->sale_price < $this->price,
            'formatted_price' => '$' . number_format($this->price, 2),

            // Call model methods
            'average_rating' => round($this->averageRating(), 1),
            'stock_status' => $this->getStockStatus(),

            // Complex computed values
            'savings' => $this->when($this->sale_price, function () {
                return [
                    'amount' => $this->price - $this->sale_price,
                    'percentage' => $this->calculateDiscountPercentage(),
                ];
            }),
        ];
    }

    protected function calculateDiscountPercentage()
    {
        if (!$this->sale_price || $this->sale_price >= $this->price) {
            return 0;
        }

        return round((($this->price - $this->sale_price) / $this->price) * 100);
    }
}

Resource Responses and Status Codes

Resources can include custom HTTP status codes and headers:

Custom Responses:
// In controller - custom status codes
public function store(Request $request)
{
    $user = User::create($request->validated());

    return (new UserResource($user))
        ->response()
        ->setStatusCode(201)
        ->header('X-Resource-Created', true);
}

public function update(Request $request, $id)
{
    $user = User::findOrFail($id);
    $user->update($request->validated());

    return (new UserResource($user))
        ->response()
        ->setStatusCode(200);
}

// Additional response wrapper
public function show($id)
{
    $user = User::findOrFail($id);

    return (new UserResource($user))
        ->additional([
            'message' => 'User retrieved successfully',
            'status' => 'success'
        ]);
}

// Response:
{
    "data": {
        "id": 1,
        "name": "John Doe"
    },
    "message": "User retrieved successfully",
    "status": "success"
}
Practice Exercise 1: Create a BlogPostResource that includes: id, title, excerpt, author (nested resource), category name, published date (formatted as "F j, Y"), read time (computed from content length), and comments count. Only include edit_url if the current user is the author.
Practice Exercise 2: Build an OrderResource that includes: order number, status, total, items (nested collection), customer details, and shipping address. Add a "can_cancel" computed boolean based on order status and created date. Include a "tracking" object only if the order has been shipped.
Practice Exercise 3: Create a UserProfileResource with pagination that returns users with their post count, follower count, and following count. Add custom meta data including total_active_users, average_posts_per_user, and generated_at timestamp. Customize the pagination wrapper to use "users" instead of "data".

Summary

API Resources provide a powerful transformation layer for your API responses:

  • Consistent Structure: Maintain predictable API response formats
  • Conditional Attributes: Include/exclude data based on permissions or context
  • Computed Values: Add calculated attributes without modifying models
  • Relationship Management: Handle nested resources and avoid N+1 queries
  • Pagination Support: Automatic meta data for paginated results
  • Flexibility: Add custom meta data, links, and headers

In the next lesson, we'll explore Laravel's Service Container and Dependency Injection to understand how Laravel manages class dependencies.