Laravel API Resources: Transforming Data
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.
- 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.
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:
# 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:
<?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),
];
}
}
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:
<?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:
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());
}
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:
<?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';
}
}
Resource Collections
When returning multiple models, you can use either the ::collection() static method or create a dedicated ResourceCollection class for more control:
<?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:
// 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:
// 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
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
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.
Use whenLoaded() for Relationships
Prevent N+1 queries by always using whenLoaded() for relationships. This ensures relationships are only serialized when explicitly eager loaded.
Create Separate Resources for Different Contexts
Consider creating UserResource for public data and UserDetailResource for authenticated user data instead of overloading conditional logic.
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.
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 methods —
when(),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.
