laravelmastery
SnippetsArticlesSeriesNews

Eloquent, properly · Lesson 3 of 3

Eloquent, properly · Lesson 3

API resources without leaking attributes

Query it safely and fastEloquentQueries

The index kept the staff note off the list query. The model still has attributes the API should not repeat: the team id you already scoped, and a staff note. A resource is the list of fields a client is allowed to see. A new column stays private until someone adds it to that list on purpose.

Video

01 Map the invoice, then its lines

whenLoaded() means the lines are included when the controller eager-loaded them, and omitted when it did not. That keeps a list endpoint small and a show endpoint complete. The status cast from earlier in the series means status is an enum here, so the resource sends the backed string with value. issued_on is a Carbon date, and toDateString() sends the calendar day rather than a timestamp the client did not ask for.

InvoiceResource.phpPHP
class InvoiceResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'number' => $this->number,
            'issued_on' => $this->issued_on->toDateString(),
            'status' => $this->status->value,
            'lines' => InvoiceLineResource::collection($this->whenLoaded('lines')),
        ];
    }
}

The controller chooses the shape by choosing the load. The list eager-loads lines only when the client of this endpoint is supposed to see them. A list that is numbers and statuses omits with('lines'), and whenLoaded() then omits the key. MissingAttributeException and the lazy-load guard never enter into it, because the resource does not touch a relation that was not loaded.

PHP
return InvoiceResource::collection(
    Invoice::query()->forTeam($team)->with('lines')->paginate(20)
);

Do not return $invoice->toArray() from an API. It will grow every time someone adds a column, and the new column is public the moment the migration runs. $hidden on the model is easy to bypass, because a different path can call toArray() or return the model from a job payload. The resource returned by this controller is the list you can review. team_id is absent on purpose. The client already asked in the context of a team, and repeating the key invites a client to trust it as an authorisation check.

02 Write the line resource, and do not touch lines directly

InvoiceLineResource is the same kind of list, one level down. description and amount are the fields a client needs to render a line. amount stays an integer of pence. The resource does not divide by 100, so every client shares the same unit the database stores. A display rule belongs in one formatter, not in a second copy of the resource that silently changes the number.

InvoiceLineResource.phpPHP
class InvoiceLineResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'description' => $this->description,
            'amount' => $this->amount,
        ];
    }
}

The trap is reading $this->lines instead of $this->whenLoaded('lines'). The property lazy-loads. On the list, that is one query per invoice, and outside production the guard from the previous lesson throws LazyLoadingViolationException. whenLoaded() is how the resource refuses that query. If the show endpoint must include lines, the show action calls loadMissing('lines') or with('lines') before it builds the resource. The resource stays honest either way.

A nested team resource has the same rule. Do not embed the whole team so the client can see a name, unless this response eager-loaded team and a TeamResource lists the fields that are safe. Stuffing $this->team->toArray() into the invoice publishes every team column. Leave notes and foreign keys off both resources until a client has a reason to see them.

Recap

You can decide the invoice fields a client may see, map each line through its own resource, and keep a missing eager load from becoming a lazy query inside toArray().

  • toArray() on the model publishes every attribute you add later.
  • whenLoaded('lines') keeps a list endpoint small.
  • The controller chooses the shape by choosing the eager load.
  • InvoiceLineResource lists description and amount. Reading $this->lines lazy-loads the relation the guard is there to catch.

Check yourself · 3 questions

Question 1 of 3Someone adds payment_attempt to the invoices table. The API returns $invoice->toArray(). Who can see the new column?

Where the series leaves you

The invoice now has a path through the series.

  • The model and the fillable list.
  • The constrained foreign keys.
  • The relationships, and the forTeam() scope.
  • The two-query list behind the InvoiceResource.

Test-driven Laravel is how you prove all of it.

← PreviousIndexes, select and chunking

Back to the overview

On this page

  • Map the invoice, then its lines
  • Write the line resource, and do not touch lines directly
  • Recap
  • Check yourself

Eloquent, properly

  • 1 Overview

Shape the data

  • 2 Models that match the domain
  • 3 Migrations you can still read next year
  • 4 Casts, enums and custom casters

Connect it

  • 5 One to many, without the surprises
  • 6 Belongs-to and whereBelongsTo
  • 7 Many to many and pivot data
  • 8 Has many through

Query it safely and fast

  • 9 Local and global scopes
  • 10 Eager loading on purpose
  • 11 Catching N+1 queries
  • 12 Indexes, select and chunking
  • 13 API resources without leaking attributes

Eloquent, properly

Lessons

  1. 01Overview

Shape the data

  1. 02Models that match the domain
  2. 03Migrations you can still read next year
  3. 04Casts, enums and custom casters

Connect it

  1. 05One to many, without the surprises
  2. 06Belongs-to and whereBelongsTo
  3. 07Many to many and pivot data
  4. 08Has many through

Query it safely and fast

  1. 09Local and global scopes
  2. 10Eager loading on purpose
  3. 11Catching N+1 queries
  4. 12Indexes, select and chunking
  5. 13API resources without leaking attributes

Get the Friday email

One snippet and the week’s Laravel news, in your inbox every Friday morning. Unsubscribe whenever you like.

laravelmastery

Practical Laravel learning for working developers, made in the UK.

Learn

  • Snippets
  • Articles
  • Series

Stay current

  • News
  • Friday email
  • RSS feed

Company

  • About
  • Privacy
  • Terms
© 2026 Laravel Mastery. Independent and not affiliated with Laravel Holdings Inc.Designed and built by Half Shell Studios

We remember the videos you watch, where you left off, and which series lessons you mark as read, against this connection’s address. Nothing is stored until you agree.