API Resources
Laravel API resources and transformers for building consistent, well-structured JSON API responses
You are an expert in Laravel API resources for building Laravel applications. You treat the transformation layer as a critical API contract boundary, ensuring that internal data structures never leak to consumers and that every response follows a consistent, predictable format. ## Key Points - Always use API Resources instead of returning models directly -- it prevents accidental exposure of sensitive columns. - Use `whenLoaded()` to avoid N+1 queries: the relationship is only serialized when it has been eager-loaded. - Create a dedicated `ResourceCollection` when you need custom pagination metadata or response wrapping. - Keep resource classes thin: formatting logic (date formats, URL generation) belongs here; business logic does not. - Use consistent envelope patterns (`data`, `meta`, `links`) across all endpoints. - Version your resources when breaking changes are needed; a `V2\PostResource` can coexist with `V1\PostResource`. - **Exposing sensitive fields**: Returning `$this->resource->toArray()` or `parent::toArray()` leaks every column. Always define fields explicitly. - **N+1 queries through resources**: If a resource accesses `$this->author->name` but the relationship was not eager-loaded, each row triggers a query. Use `whenLoaded()`. - **Circular references**: A `PostResource` includes `UserResource`, which includes `PostResource` again. Break the cycle with conditional includes or separate list/detail resources. - **Pagination lost**: Using `->get()` then `PostResource::collection()` loses pagination. Use `->paginate()` to preserve automatic pagination metadata. - **Inconsistent date formats**: Choose one format (ISO 8601) and use it everywhere. Do not return raw Carbon objects. - **Forgetting `additional()`**: When you need per-request metadata (permissions, feature flags), use `->additional()` rather than adding it to every resource's `toArray()`.
skilldb get php-laravel-skills/API ResourcesFull skill: 309 linesAPI Resources & Transformers — PHP/Laravel
You are an expert in Laravel API resources for building Laravel applications. You treat the transformation layer as a critical API contract boundary, ensuring that internal data structures never leak to consumers and that every response follows a consistent, predictable format.
Core Philosophy
The API response is the product. Internal models, database schemas, and Eloquent relationships are implementation details that should never dictate the shape of what clients receive. API Resources exist as a deliberate transformation boundary between the application's internal representation and its public contract. When a resource class explicitly lists every field it returns, the team controls exactly what is exposed, when it changes, and how breaking changes are communicated. When resources are skipped and models are returned directly, every column addition, relationship change, or cast modification becomes an accidental API change that can break clients without warning.
Consistency across endpoints builds developer trust. Every endpoint should return data in the same envelope format, use the same date serialization, follow the same pagination structure, and report errors with the same shape. API Resources and Resource Collections make this consistency mechanical rather than aspirational. When a new endpoint is added, the developer reaches for the same patterns -- data, meta, links -- and consumers can parse responses without consulting endpoint-specific documentation. Inconsistency forces every consumer to write special-case parsing logic, which multiplies the cost of every API change.
Performance and correctness in the resource layer depend on what happens before it. A resource that accesses $this->author->name is correct, but if the relationship was not eager-loaded, it triggers a query for every item in a collection. The whenLoaded() method is not a convenience -- it is a correctness guard that prevents N+1 queries from hiding inside innocent-looking transformations. The resource layer should never be the source of unexpected database queries; all data access should be resolved before the resource is constructed.
Overview
API Resources provide a transformation layer between Eloquent models and the JSON responses returned by your API. They let you control exactly which fields are exposed, format data consistently, include conditional relationships, and add metadata -- all without cluttering controllers.
Core Concepts
Basic Resource
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' => str($this->body)->limit(200),
'body' => $this->when($request->routeIs('posts.show'), $this->body),
'status' => $this->status,
'author' => new UserResource($this->whenLoaded('author')),
'tags' => TagResource::collection($this->whenLoaded('tags')),
'comments_count' => $this->whenCounted('comments'),
'published_at' => $this->published_at?->toIso8601String(),
'created_at' => $this->created_at->toIso8601String(),
'updated_at' => $this->updated_at->toIso8601String(),
'links' => [
'self' => route('api.posts.show', $this->resource),
],
];
}
}
Resource Collection
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
class PostCollection extends ResourceCollection
{
public $collects = PostResource::class;
public function toArray(Request $request): array
{
return [
'data' => $this->collection,
];
}
public function with(Request $request): array
{
return [
'meta' => [
'api_version' => 'v1',
'generated_at' => now()->toIso8601String(),
],
];
}
}
// Usage in controller
public function index()
{
$posts = Post::published()
->with('author', 'tags')
->withCount('comments')
->latest()
->paginate(15);
return new PostCollection($posts);
}
Returning Resources from Controllers
class PostController extends Controller
{
public function index(Request $request)
{
$posts = Post::published()
->with('author')
->withCount('comments')
->latest()
->paginate($request->integer('per_page', 15));
return PostResource::collection($posts);
}
public function show(Post $post)
{
$post->load(['author', 'tags', 'comments.user']);
return new PostResource($post);
}
public function store(StorePostRequest $request)
{
$post = $request->user()->posts()->create($request->validated());
return (new PostResource($post))
->response()
->setStatusCode(201);
}
}
Implementation Patterns
Conditional Fields and Relationships
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'email' => $this->when($request->user()?->isAdmin(), $this->email),
// Only include if relationship was eager-loaded
'profile' => new ProfileResource($this->whenLoaded('profile')),
'roles' => RoleResource::collection($this->whenLoaded('roles')),
// Only include if count was loaded
'posts_count' => $this->whenCounted('posts'),
// Merge conditionally
$this->mergeWhen($request->user()?->id === $this->id, [
'api_token' => $this->api_token,
'notification_preferences' => $this->notification_preferences,
]),
];
}
Nested Resources
class CommentResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'body' => $this->body,
'user' => new UserResource($this->whenLoaded('user')),
'replies' => CommentResource::collection($this->whenLoaded('replies')),
'created_at' => $this->created_at->toIso8601String(),
];
}
}
Wrapping and Pagination
// Disable the "data" wrapper globally (optional)
// In AppServiceProvider::boot()
JsonResource::withoutWrapping();
// Or customize the wrapper per resource
class PostResource extends JsonResource
{
public static $wrap = 'post';
}
// Pagination is automatic with paginate()
// Response includes: data, links (first, last, prev, next), meta (current_page, total, etc.)
Resource with Additional Data
return (new PostResource($post))
->additional([
'meta' => [
'allowed_actions' => [
'can_edit' => $request->user()->can('update', $post),
'can_delete' => $request->user()->can('delete', $post),
],
],
]);
API Response Consistency Pattern
// Trait for consistent error responses
namespace App\Http\Traits;
trait ApiResponses
{
protected function success(mixed $data, int $status = 200)
{
return response()->json($data, $status);
}
protected function error(string $message, int $status, array $errors = [])
{
return response()->json([
'message' => $message,
'errors' => $errors ?: (object) [],
], $status);
}
}
// Global exception handler for consistent error format
// In bootstrap/app.php (Laravel 11+)
->withExceptions(function (Exceptions $exceptions) {
$exceptions->shouldRenderJsonWhen(fn (Request $request) =>
$request->is('api/*') || $request->expectsJson()
);
$exceptions->render(function (NotFoundHttpException $e, Request $request) {
if ($request->expectsJson()) {
return response()->json([
'message' => 'Resource not found.',
], 404);
}
});
})
Filtering and Sparse Fieldsets
class PostController extends Controller
{
public function index(Request $request)
{
$posts = Post::query()
->with('author')
->when($request->filled('status'), fn ($q) =>
$q->where('status', $request->input('status'))
)
->when($request->filled('author'), fn ($q) =>
$q->where('user_id', $request->input('author'))
)
->when($request->filled('search'), fn ($q) =>
$q->where('title', 'like', '%' . $request->input('search') . '%')
)
->when($request->filled('sort'), fn ($q) =>
$q->orderBy(
ltrim($request->input('sort'), '-'),
str_starts_with($request->input('sort'), '-') ? 'desc' : 'asc'
)
, fn ($q) => $q->latest())
->paginate($request->integer('per_page', 15));
return PostResource::collection($posts);
}
}
Best Practices
- Always use API Resources instead of returning models directly -- it prevents accidental exposure of sensitive columns.
- Use
whenLoaded()to avoid N+1 queries: the relationship is only serialized when it has been eager-loaded. - Create a dedicated
ResourceCollectionwhen you need custom pagination metadata or response wrapping. - Keep resource classes thin: formatting logic (date formats, URL generation) belongs here; business logic does not.
- Use consistent envelope patterns (
data,meta,links) across all endpoints. - Version your resources when breaking changes are needed; a
V2\PostResourcecan coexist withV1\PostResource.
Common Pitfalls
- Exposing sensitive fields: Returning
$this->resource->toArray()orparent::toArray()leaks every column. Always define fields explicitly. - N+1 queries through resources: If a resource accesses
$this->author->namebut the relationship was not eager-loaded, each row triggers a query. UsewhenLoaded(). - Circular references: A
PostResourceincludesUserResource, which includesPostResourceagain. Break the cycle with conditional includes or separate list/detail resources. - Pagination lost: Using
->get()thenPostResource::collection()loses pagination. Use->paginate()to preserve automatic pagination metadata. - Inconsistent date formats: Choose one format (ISO 8601) and use it everywhere. Do not return raw Carbon objects.
- Forgetting
additional(): When you need per-request metadata (permissions, feature flags), use->additional()rather than adding it to every resource'stoArray().
Anti-Patterns
-
Model passthrough resources — creating a resource class that simply calls
parent::toArray($request)or$this->resource->toArray(), exposing every database column including internal IDs, timestamps, and soft-delete markers. If the resource does not explicitly control its output, it provides no value over returning the model directly. -
Business logic in resources — performing calculations, calling services, or making authorization decisions inside
toArray(). Resources should format and filter data, not compute it. Move business logic to the service layer and pass the computed result to the resource. -
Inconsistent date formats — some endpoints returning
Y-m-d, others returning Unix timestamps, others returning full Carbon string representations. Standardize on ISO 8601 (toIso8601String()) in every resource and enforce it through code review or a base resource class. -
Eager loading inside resources — accessing relationships inside
toArray()withoutwhenLoaded(), causing N+1 queries that are invisible until the response time spikes in production. Every relationship access in a resource should be guarded bywhenLoaded(), and the controller or service should handle eager loading. -
Versioning through conditionals — adding
if ($request->header('API-Version') === 'v2')branches inside a single resource class. This creates unmaintainable spaghetti as versions accumulate. Create separate resource classes per version (V1\PostResource,V2\PostResource) and route to the correct one at the controller level.
Install this skill directly: skilldb add php-laravel-skills
Related Skills
Authentication
Laravel authentication using Sanctum for API tokens and SPAs, and Fortify for web-based auth flows
Blade Livewire
Blade templating engine and Livewire for building dynamic server-rendered UI in Laravel applications
Deployment
Deploying Laravel applications with Forge, Vapor, and general server deployment strategies
Eloquent ORM
Eloquent ORM patterns for models, relationships, query scoping, and database interactions in Laravel
Queues Jobs
Laravel queues, jobs, and background task processing with Redis, SQS, and Horizon
Routing Middleware
Laravel routing, route groups, resource controllers, and middleware for request filtering and authentication