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:
- Create a UserResource that hides email unless viewing own profile
- Create a ProductResource with computed discount_percentage
- Build a CommentCollection with statistics metadata
- Implement conditional relationship loading via ?include= parameter
- 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.