Raw Eloquent models expose too much. API Resources transform models to clean, consistent JSON responses. At ZIRA Software, resources power APIs serving millions of requests with consistent formatting.
Why API Resources?
Without resources:
// Exposes all model attributes including sensitive data
Route::get('/users', function () {
return User::all(); // Includes password hash, remember_token, etc.
});
With resources:
// Controlled, transformed response
Route::get('/users', function () {
return UserResource::collection(User::all());
});
Creating Resources
php artisan make:resource UserResource
// app/Http/Resources/UserResource.php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class UserResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'created_at' => $this->created_at->toDateTimeString(),
'posts_count' => $this->posts->count(),
];
}
}
Resource Collections
Create collection:
php artisan make:resource UserCollection
// app/Http/Resources/UserCollection.php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\ResourceCollection;
class UserCollection extends ResourceCollection
{
public function toArray($request)
{
return [
'data' => $this->collection,
'meta' => [
'total_users' => $this->collection->count(),
'verified_users' => $this->collection->where('email_verified_at', '!=', null)->count(),
],
];
}
}
Usage:
// Single resource
Route::get('/users/{user}', function (User $user) {
return new UserResource($user);
});
// Collection
Route::get('/users', function () {
return UserResource::collection(User::paginate(15));
});
// Custom collection
Route::get('/users', function () {
return new UserCollection(User::all());
});
Conditional Attributes
Show attributes based on conditions:
// app/Http/Resources/UserResource.php
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
// Only include when authenticated user is admin
'admin_notes' => $this->when($request->user()->isAdmin(), $this->admin_notes),
// Only include when relationship is loaded
'posts' => PostResource::collection($this->whenLoaded('posts')),
// Only include when pivot exists
'subscription' => $this->whenPivotLoaded('user_subscriptions', function () {
return [
'subscribed_at' => $this->pivot->created_at,
'plan' => $this->pivot->plan,
];
}),
];
}
Nested Relationships
// app/Http/Resources/PostResource.php
public function toArray($request)
{
return [
'id' => $this->id,
'title' => $this->title,
'content' => $this->content,
'published_at' => $this->published_at,
// Nested resource
'author' => new UserResource($this->whenLoaded('author')),
// Collection of resources
'comments' => CommentResource::collection($this->whenLoaded('comments')),
];
}
Pagination
Automatic pagination wrapping:
Route::get('/posts', function () {
return PostResource::collection(Post::paginate(15));
});
// Returns:
// {
// "data": [...],
// "links": {
// "first": "http://...",
// "last": "http://...",
// "prev": null,
// "next": "http://..."
// },
// "meta": {
// "current_page": 1,
// "from": 1,
// "last_page": 5,
// "path": "http://...",
// "per_page": 15,
// "to": 15,
// "total": 73
// }
// }
Response Wrapping
Customize wrapper:
// app/Http/Resources/UserResource.php
public function with($request)
{
return [
'meta' => [
'version' => '1.0.0',
'timestamp' => now()->toIso8601String(),
],
];
}
Disable wrapping:
// app/Providers/AppServiceProvider.php
use Illuminate\Http\Resources\Json\JsonResource;
public function boot()
{
JsonResource::withoutWrapping();
}
Additional Metadata
// app/Http/Resources/PostResource.php
public function toArray($request)
{
return [
'id' => $this->id,
'title' => $this->title,
'content' => $this->content,
];
}
public function with($request)
{
return [
'meta' => [
'generated_at' => now(),
'request_id' => $request->header('X-Request-ID'),
],
];
}
Conditional Relationships
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
// Include posts only when requested via ?include=posts
'posts' => PostResource::collection(
$this->whenLoaded('posts')
),
// Include aggregates only when loaded
'posts_count' => $this->when(
isset($this->posts_count),
$this->posts_count
),
];
}
Error Responses
// app/Exceptions/Handler.php
use Illuminate\Validation\ValidationException;
public function render($request, Exception $exception)
{
if ($exception instanceof ValidationException) {
return response()->json([
'message' => 'Validation failed',
'errors' => $exception->errors(),
], 422);
}
if ($exception instanceof ModelNotFoundException) {
return response()->json([
'message' => 'Resource not found',
], 404);
}
return parent::render($request, $exception);
}
Performance Considerations
Eager load relationships:
// Bad - N+1 queries
Route::get('/posts', function () {
return PostResource::collection(Post::all());
});
// Good - Single query
Route::get('/posts', function () {
return PostResource::collection(
Post::with(['author', 'comments'])->get()
);
});
Conclusion
API Resources provide consistent, controlled JSON responses. Conditional attributes and nested resources create flexible, efficient APIs.
Need API development expertise? Contact ZIRA Software for RESTful API architecture.