REST API Development

API Resources & Transformations

18 min Lesson 7 of 35

Understanding API Resources in Laravel

API Resources provide a transformation layer between your Eloquent models and the JSON responses sent to your API consumers. They give you precise control over which attributes are exposed and how they are formatted.

Why Use API Resources?

Without API Resources, you might return raw model data that exposes sensitive fields or has inconsistent formatting. Resources solve these problems by:

  • Controlling which model attributes are exposed in responses
  • Transforming data format consistently across your API
  • Adding computed properties to responses
  • Creating reusable transformation logic
  • Separating data representation from business logic
Note: API Resources are particularly important for maintaining backward compatibility when your database schema evolves.

Creating a Basic Resource

Generate a resource using Artisan:

php artisan make:resource PostResource

This creates a file in 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->content, 'published_at' => $this->published_at?->format('Y-m-d H:i:s'), 'created_at' => $this->created_at->format('Y-m-d H:i:s'), 'updated_at' => $this->updated_at->format('Y-m-d H:i:s'), ]; } }

Using Resources in Controllers

Return a single resource from your controller:

use App\Http\Resources\PostResource; use App\Models\Post; public function show(Post $post) { return new PostResource($post); }

Laravel automatically wraps the resource in a data key:

{ "data": { "id": 1, "title": "Introduction to Laravel APIs", "slug": "introduction-to-laravel-apis", "excerpt": "Learn how to build RESTful APIs...", "content": "Full article content...", "published_at": "2024-01-15 10:30:00", "created_at": "2024-01-10 08:00:00", "updated_at": "2024-01-15 10:30:00" } }

Resource Collections

For collections of resources, use the collection method:

public function index() { $posts = Post::with('author', 'category') ->latest() ->paginate(15); return PostResource::collection($posts); }

Or create a dedicated collection class for more control:

php artisan make:resource PostCollection
<?php namespace App\Http\Resources; use Illuminate\Http\Resources\Json\ResourceCollection; class PostCollection extends ResourceCollection { public function toArray(Request $request): array { return [ 'data' => $this->collection, 'meta' => [ 'total' => $this->total(), 'per_page' => $this->perPage(), 'current_page' => $this->currentPage(), ], ]; } }
Tip: Custom collection classes give you complete control over pagination metadata and collection-level information.

Conditional Attributes

Include attributes conditionally based on logic:

public function toArray(Request $request): array { return [ 'id' => $this->id, 'title' => $this->title, 'content' => $this->content, // Only include draft status if user is authenticated 'is_draft' => $this->when( $request->user(), $this->is_draft ), // Only show edit_url if user owns the post 'edit_url' => $this->when( $request->user()?->id === $this->user_id, route('posts.edit', $this->id) ), ]; }

Merging Conditional Attributes

Use mergeWhen() to conditionally add multiple attributes:

public function toArray(Request $request): array { return [ 'id' => $this->id, 'title' => $this->title, // Merge admin-only fields if user is admin $this->mergeWhen($request->user()?->is_admin, [ 'internal_notes' => $this->internal_notes, 'moderation_status' => $this->moderation_status, 'revision_count' => $this->revisions->count(), ]), ]; }
Note: mergeWhen() is perfect for role-based attribute visibility.

Nested Relationships

Include related resources with proper transformation:

public function toArray(Request $request): array { return [ 'id' => $this->id, 'title' => $this->title, // Transform nested relationships 'author' => new UserResource($this->whenLoaded('author')), 'category' => new CategoryResource($this->whenLoaded('category')), 'tags' => TagResource::collection($this->whenLoaded('tags')), // Counts without loading relationships 'comments_count' => $this->whenCounted('comments'), 'average_rating' => $this->whenAggregated('ratings', 'rating', 'avg'), ]; }
Warning: Always use whenLoaded() to prevent N+1 query problems.

Adding Computed Properties

Add dynamic attributes calculated at runtime:

public function toArray(Request $request): array { return [ 'id' => $this->id, 'title' => $this->title, 'content' => $this->content, // Computed properties 'reading_time' => $this->calculateReadingTime(), 'word_count' => str_word_count(strip_tags($this->content)), 'is_new' => $this->created_at->isToday(), 'url' => route('posts.show', $this->slug), 'image_url' => $this->image ? asset('storage/' . $this->image) : null, 'views_formatted' => number_format($this->views), 'published_human' => $this->published_at?->diffForHumans(), ]; }

Customizing the Resource Wrapper

By default, resources are wrapped in a data key. Customize or disable:

// Custom wrapper in resource class public static $wrap = 'post'; // Disable wrapping globally in AppServiceProvider JsonResource::withoutWrapping();

Adding Metadata to Collections

Add extra information alongside collection data:

public function index() { $posts = Post::latest()->paginate(15); return PostResource::collection($posts) ->additional([ 'meta' => [ 'api_version' => 'v1', 'timestamp' => now()->toIso8601String(), 'total_posts' => Post::count(), ] ]); }

Conditional Relationships

Load relationships based on query parameters:

public function index(Request $request) { $query = Post::query(); $includes = $request->input('include', ''); $allowedIncludes = ['author', 'category', 'tags']; $requestedIncludes = array_filter( explode(',', $includes), fn($inc) => in_array($inc, $allowedIncludes) ); if (!empty($requestedIncludes)) { $query->with($requestedIncludes); } return PostResource::collection($query->paginate(15)); }
Tip: Allowing clients to request specific relationships reduces over-fetching.

Pivot Data in Relationships

Include pivot table data in many-to-many relationships:

public function toArray(Request $request): array { return [ 'id' => $this->id, 'title' => $this->title, 'tags' => $this->whenLoaded('tags', function() { return $this->tags->map(function($tag) { return [ 'id' => $tag->id, 'name' => $tag->name, 'assigned_at' => $tag->pivot->created_at, ]; }); }), ]; }

Response Customization

Customize the entire response with the with() method:

class PostResource extends JsonResource { public function toArray(Request $request): array { return [ 'id' => $this->id, 'title' => $this->title, ]; } public function with(Request $request): array { return [ 'success' => true, 'message' => 'Post retrieved successfully', ]; } }

Polymorphic Resources

Handle polymorphic relationships with conditional resource types:

public function toArray(Request $request): array { return [ 'id' => $this->id, 'content' => $this->content, 'commentable' => $this->whenLoaded('commentable', function() { return match($this->commentable_type) { 'App\\Models\\Post' => new PostResource($this->commentable), 'App\\Models\\Video' => new VideoResource($this->commentable), default => null, }; }), ]; }

Resource Response Modification

Modify HTTP response headers before sending:

public function withResponse(Request $request, JsonResponse $response): void { $response->header('X-Resource-Type', 'Post'); $response->setMaxAge(3600); $response->setPublic(); }
Exercise:
  1. Create a UserResource that hides email unless viewing own profile
  2. Create a ProductResource with computed discount_percentage
  3. Build a CommentCollection with statistics metadata
  4. Implement conditional relationship loading via ?include= parameter
  5. Add pivot data to a many-to-many relationship

Best Practices

  • Use whenLoaded(): Prevent N+1 queries with conditional loading
  • Hide Sensitive Data: Never expose passwords or tokens
  • Consistent Formatting: Format dates and numbers uniformly
  • Computed Properties: Add helpful fields like URLs and human dates
  • Version Resources: Create separate resources for API versions
  • Test Transformations: Verify expected JSON structure

Summary

API Resources provide powerful control over response transformation. They hide implementation details, add computed properties, and create consistent, predictable API responses. Next, we'll explore request validation to ensure data integrity.