Skip to content
ExponaBLOG
PHP & LARAVEL DEVELOPMENT · DAY 014 / 100

Laravel API Resources: a practical guide for developers

Control your API output.

Expona Admin7 min read

The problem with raw models in APIs

When you build an API endpoint that returns user data, it's tempting to grab a model from the database and serialize it directly. After all, Laravel's Eloquent makes this almost too easy. But we've all seen the fallout: passwords end up in JSON responses, internal timestamps leak, and every time you add a column to a table, it automatically exposes itself to clients whether you intended it or not. Laravel API Resources: a practical guide for developers walks you through the right way to control your API output, ensuring you return only what clients need and nothing they shouldn't see.

The core principle is simple but often overlooked: never return models directly from your API endpoints. Always use a Resource layer to transform and filter what goes over the wire.

Never return models directly

Returning a raw model is a security and maintenance liability. When you do:

php
Route::get('/users/{id}', function ($id) {
    return User::find($id);
});

Laravel serializes the entire model, including all its attributes and any hidden fields you've defined with $hidden. But hidden fields only protect you if you never forget to add to them. As your application grows and your database schema evolves, you're one oversight away from leaking sensitive data.

A Resource acts as a translator between your internal model and the outside world. Instead of exposing the raw model, you deliberately choose what to expose and how to transform it.

php
// Generate a resource with: php artisan make:resource UserResource

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,
        ];
    }
}

Now your endpoint becomes:

php
Route::get('/users/{id}', function ($id) {
    return new UserResource(User::find($id));
});

The model has attributes like password_hash, remember_token, and api_key, but none of those appear in the response. You're in control. This is the foundation of a secure and maintainable API.

Hide internal fields

As APIs grow, you often need different representations of the same model depending on context. A user listing might show only essential fields, while a user detail view might include metadata. Resources let you make these choices explicit and intentional.

Consider a real-world scenario: you're building an e-commerce platform with orders. The Order model holds cost calculations, warehouse IDs, internal notes, and supplier information. When you return an order to a customer, you want them to see the order total and items, but not the wholesale cost or internal supplier notes.

php
namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

class OrderResource extends JsonResource
{
    public function toArray($request)
    {
        return [
            'id' => $this->id,
            'order_number' => $this->order_number,
            'total' => money_format($this->total),
            'items' => OrderItemResource::collection($this->items),
            'status' => $this->status,
            'created_at' => $this->created_at->toIso8601String(),
            // note: wholesale_cost, supplier_id, internal_notes are never included
        ];
    }
}

But when your admin dashboard or internal reporting system queries the same endpoint, you might need a different shape. Instead of duplicating logic, you could create an AdminOrderResource that includes the sensitive fields, or you could use a query parameter to control the output. The point is that the Resource layer gives you a clean place to make these decisions.

This approach scales well. When your team grows and new developers join, the Resource definition is the contract. It documents what an API consumer should expect and protects the codebase by preventing accidental leaks.

Conditional fields with whenLoaded

One of the most powerful features in Laravel Resources is the whenLoaded method. It solves a practical problem: you want to include related data in some contexts but not others, and you don't want to waste database queries.

Imagine a User has Posts, and a Post has Comments. If you always eager-load comments when you retrieve a post, your API becomes slow when clients don't need comments. But if you never eager-load, you risk N+1 queries when a client does request the full tree.

The solution is conditional inclusion based on what was actually loaded:

php
namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

class PostResource extends JsonResource
{
    public function toArray($request)
    {
        return [
            'id' => $this->id,
            'title' => $this->title,
            'body' => $this->body,
            'author' => new UserResource($this->author),
            'comments' => CommentResource::collection($this->whenLoaded('comments')),
            'comment_count' => $this->when(
                $this->relationLoaded('comments'),
                $this->comments->count()
            ),
        ];
    }
}

Now you can use it flexibly. If a controller loads posts without comments, the comments key won't appear in the JSON. If comments were eager-loaded, they'll be included:

php
// Lightweight endpoint
Route::get('/posts', function () {
    return PostResource::collection(Post::paginate());
});

// Detail endpoint with comments
Route::get('/posts/{id}', function ($id) {
    return new PostResource(Post::with('comments')->find($id));
});

This pattern is especially valuable in SaaS platforms where you might have multi-level nested data. A lead record might have associated contacts, deals, interactions, and notes. Including all of that in every response is wasteful, but clients sometimes need the full context. Using whenLoaded makes this efficient and explicit.

Resource collections handle pagination meta

When you return a collection of resources from a paginated query, Laravel handles the transformation automatically. But there's more going on under the hood than many developers realize.

php
Route::get('/users', function () {
    return UserResource::collection(User::paginate());
});

This returns not just the transformed users, but also pagination metadata:

php
{
  "data": [
    { "id": 1, "name": "Alice", ... },
    { "id": 2, "name": "Bob", ... }
  ],
  "links": {
    "first": "http://api.local/users?page=1",
    "last": "http://api.local/users?page=5",
    "prev": null,
    "next": "http://api.local/users?page=2"
  },
  "meta": {
    "current_page": 1,
    "from": 1,
    "last_page": 5,
    "path": "http://api.local/users",
    "per_page": 15,
    "to": 15,
    "total": 75
  }
}

This structure is what clients expect from a professional API. If you return a raw collection or paginator, you get the same metadata but your individual user objects expose sensitive fields. By wrapping the collection in a Resource, you get both benefits: transformed individual records and proper pagination envelope.

You can also customize the wrapper if needed. For a trading platform that returns paginated price ticks, you might want to include additional metadata like the timestamp of the data fetch or a hash of the page for consistency checks:

php
namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\ResourceCollection;

class TickCollection extends ResourceCollection
{
    public function toArray($request)
    {
        return [
            'data' => $this->collection,
            'meta' => [
                'current_page' => $this->currentPage(),
                'total' => $this->total(),
                'fetched_at' => now()->toIso8601String(),
                'sequence_id' => hash('sha256', json_encode($this->collection)),
            ],
        ];
    }
}

Then use it with:

php
return new TickCollection(PriceTick::paginate());

Common mistakes

Mistake 1: Nesting resources too deeply. If you eager-load a Post with its Author, Comments, and each Comment's Replies and Author, your Resource can end up serializing ten levels of nested data. This is wasteful and slow. Be deliberate about what you include and at what depth. Use pagination at each level if you have many nested items.

Mistake 2: Forgetting to use Resource collections. Returning an array of resources works, but it bypasses pagination and loses the proper envelope structure. Always use ResourceCollection or the helper method Resource::collection() when returning multiple items.

Mistake 3: Putting business logic in the Resource. Resources are for transformation and serialization, not for computing permissions or filtering based on user roles. Do that in your controller or a policy. A Resource's job is to say "given this model, how do I represent it?" not "should this user see this field?"

Mistake 4: Ignoring the $request parameter. The Resource's toArray($request) method receives the request object. You can use it to include different fields based on query parameters, authentication status, or other context. Many developers ignore this and hardcode the same structure for all callers.

Key takeaways

  • Always use a Resource to transform models before returning them from API endpoints. This is your primary defense against accidental data leaks and makes your API contract explicit.
  • Use whenLoaded and when methods to conditionally include fields based on what was actually loaded and the request context. This prevents N+1 queries and keeps responses lean.
  • Use Resource collections (not raw model collections) for paginated endpoints to get the proper envelope with links and metadata.
  • Keep Resources focused on transformation. Move business logic, authentication checks, and filtering to controllers and policies.
  • Document what each Resource returns. Your teammates and your future self will appreciate knowing exactly what fields are exposed and why.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles