Profile    Mohammed Shiroz Status   Loading  
Logo
Share This
Back to blog
Filter by:
Tags
//Article title

Laravel API Resources: Shaping JSON Responses the Right Way

About Post

return $contract; is the most tempting line in Laravel. It works, it's fast to write, and the JSON looks fine in Postman. It's also a quiet decision to make your database schema your public API, forever.

Every column you add tomorrow will appear in the response. Every column you rename will break a client. And the mobile app built against that JSON can't be updated as easily as your server can. API resources fix this by putting a small, deliberate layer between your models and the outside world. Let's build one properly, step by step.

Step 1: The basic resource

php artisan make:resource ContractResource
class ContractResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->uuid,
            'reference' => $this->reference,
            'status' => $this->status,
            'monthly_rent' => $this->monthly_rent,
            'starts_on' => $this->start_date->toDateString(),
            'ends_on' => $this->end_date->toDateString(),
            'created_at' => $this->created_at->toIso8601String(),
        ];
    }
}

Use it in a controller with return new ContractResource($contract);. Inside the resource, $this proxies to the model, so $this->reference reads the model's attribute.

Already, three good things happened. The public id is a UUID, not the auto-increment key. The field names are API names (starts_on), free to differ from the columns. And dates have a fixed, documented format. Rename start_date in the database next year and only this file changes.

The one thing to never do here is return parent::toArray($request);. That returns every attribute of the model, which is exactly what you were trying to avoid.

Step 2: Relationships without N+1 queries

A contract has a tenant and a unit. The naive version is 'tenant' => new TenantResource($this->tenant). It works, and on a list of 50 contracts it quietly runs 50 extra queries.

whenLoaded() includes a relationship only if it was already eager loaded:

'tenant' => new TenantResource($this->whenLoaded('tenant')),
'unit' => new UnitResource($this->whenLoaded('unit')),
'payments_count' => $this->whenCounted('payments'),

If the controller didn't load it, the key simply isn't in the response, and no query runs. The controller decides what's included:

$contract->load(['tenant', 'unit'])->loadCount('payments');

return new ContractResource($contract);

I like this split a lot: the resource defines how things look, the controller decides what is loaded. The resource never triggers a query by surprise.

Step 3: Conditional attributes

Some fields should only appear for certain users. Finance staff see the deposit and outstanding balance; others don't.

'deposit' => $this->when(
    $request->user()->can('viewFinancials', $this->resource),
    $this->deposit,
),

$this->mergeWhen($request->user()->can('viewFinancials', $this->resource), [
    'outstanding_balance' => $this->outstandingBalance(),
    'last_payment_on' => $this->last_payment_on?->toDateString(),
]),

When the condition is false, the key disappears from the output entirely (not null, gone). $this->resource is the underlying model, which is what a policy expects. One detail: if the value is expensive to compute, pass a closure as the second argument to when(), so it only runs when the condition is true.

A useful sibling is whenNotNull(), which skips a field when its value is null, handy for optional data like a termination date.

Step 4: Collections and pagination

For lists, use collection() with a paginator:

public function index(Request $request)
{
    $contracts = Contract::query()
        ->with('tenant')
        ->latest()
        ->paginate(20);

    return ContractResource::collection($contracts);
}

Because you passed a paginator, Laravel adds links (first, last, prev, next) and meta (current page, total, per page and so on) next to the data array automatically. Clients get a consistent shape for every list endpoint without you writing any of it. With cursorPaginate(), the meta contains cursors instead of page numbers.

If you need to change that pagination block, define a paginationInformation() method on the resource or collection class. And if you need extra top-level data on any response, ->additional(['meta' => [...]]) adds it without touching the resource.

Step 5: Decide on wrapping

By default, a single resource is wrapped in a data key: {"data": {"id": ...}}. That keeps single and list responses consistent and leaves room for meta later. I'd keep it. If your API contract says otherwise, JsonResource::withoutWrapping() in a service provider's boot() method turns it off. Paginated collections keep their data key either way, because the links and meta need somewhere to sit next to it.

The rule: a model is your storage format, a resource is your API contract. Keep them separate, and you can change your database without breaking a single client.

Three gotchas worth knowing

  • Accessing a relation directly in a resource ($this->tenant->name) quietly reintroduces N+1 queries. Use whenLoaded, or turn on Model::preventLazyLoading() in development to catch it immediately.
  • Null dates crash formatting. $this->terminated_at->toDateString() throws when it's null. Use the nullsafe operator ?->.
  • Test the shape. A feature test with assertJsonStructure() catches the day someone removes a field a mobile app depends on.

The short version

  • Never return models directly from an API.
  • Choose every field, name and format on purpose.
  • whenLoaded and whenCounted for relationships, when and mergeWhen for permissions.
  • collection() plus paginate() for consistent lists.

Do you use API resources for every endpoint, or do you reach for something else, like Fractal, DTOs or plain arrays? I'm curious what's working for people.

Comments (0)
Leave your review

Thanks for your valuable comments. Your comments has been updated and appreciate your getting in touch...

01. About Shiroz

Mohammed Shiroz

Hi, I'm Mohammed Shiroz, a software engineer and AI enthusiast from Sri Lanka who turns ideas into intelligent, real-world solutions. With over 9 years of hands-on experience, I currently lead real estate ERP development at Kate Group, a...

03.My Projects

04. Categories

Ready To order Your Project ?

Get in Touch
Close