In Laravel, what is an API resource class such as a JsonResource, and why return one instead of the Eloquent model itself?
answer
- a transformation layer between model and JSON
- php artisan make:resource WorkoutResource
- toArray(Request $request) returns the shape
- $this proxies to the wrapped model
- toResource() looks in App\Http\Resources
basics
~20 sAn API resource is a class extending JsonResource whose toArray() maps one model to the exact JSON the API promises, so column renames, new columns and internal fields do not leak into responses by accident.
solid answer
~40 sA resource is generated with `php artisan make:resource WorkoutResource` into `app/Http/Resources`, extends `Illuminate\Http\Resources\Json\JsonResource`, and returns an explicit array from `toArray(Request $request)`. Inside it, `$this->title` works because the resource forwards property reads and method calls to the wrapped model. You return it from a controller (`new WorkoutResource($workout)`, or `$workout->toResource()` in Laravel 13), and Laravel turns it into a `JsonResponse` with the outermost payload wrapped in `data`. Returning the model directly serializes whatever its own `toArray()` produces, so the response contract follows the database schema; a resource makes the contract an explicit, reviewable allow-list that can rename fields, format dates and include relations conditionally.
code
bash · 2 linesphp artisan make:resource WorkoutResource
php artisan make:resource WorkoutCollectiongo deeper
Recall the command, the base class and the one method: make:resource, JsonResource, toArray(Request). Be able to return new WorkoutResource($workout) from a controller and say the output is wrapped in data.
Explain the delegation that makes $this->column work, why the stub's parent::toArray() exposes everything, and how toResource() and #[UseResource] find the class in Laravel 13.
Argue the contract angle: a mobile API outlives app releases, so the response shape must be an explicit allow-list that a migration cannot silently widen, with model-level hiding as a backup only.
Frame resources as the versioned boundary of a public API: when to fork a V2 resource instead of editing one, and how to keep response shapes reviewable across many teams and endpoints.
## What an API resource is An **API resource** in Laravel is a small class that sits between an Eloquent model and the JSON your API sends back. It extends `Illuminate\Http\Resources\Json\JsonResource`, receives the model through its constructor (stored in the public `$resource` property), and implements one method, `toArray(Request $request)`, which returns the array that becomes the response body. The class also implements `Responsable`, so a controller can return it directly: the router calls its `toResponse()` method, which builds an `Illuminate\Http\JsonResponse`. A companion class, `ResourceCollection`, does the same job for a list of models. Think of a mobile fitness-tracker API. A `Workout` model has columns such as `id`, `user_id`, `type`, `distance_m`, `duration_s`, `device_serial` and `created_at`. The iOS and Android apps want `distance_km`, a human duration, and never the device serial. The resource is where that mapping lives. ## Creating and returning one 1. Generate the class: `php artisan make:resource WorkoutResource` writes `app/Http/Resources/WorkoutResource.php`. 2. Replace the generated body of `toArray()` with an explicit array. 3. Return it from the controller: `return new WorkoutResource($workout);` or, in Laravel 13, `return $workout->toResource();`. Two details of the generated file matter: - The stub's `toArray()` returns `parent::toArray($request)`, and the parent simply calls the wrapped model's own `toArray()`. **An untouched resource is therefore not a filter at all**: it outputs every attribute the model would. - Inside `toArray()`, `$this->distance_m` and `$this->user()` work because the base class uses `DelegatesToResource`, whose `__get`, `__isset` and `__call` forward to `$this->resource`. `toResource()` (and `toResourceCollection()` on collections and paginators) find the class by convention: for `App\Models\Workout` they try `App\Http\Resources\WorkoutResource`, then `App\Http\Resources\Workout`. A `#[UseResource(CustomWorkoutResource::class)]` attribute on the model overrides the guess, and passing a class name, `toResource(CustomWorkoutResource::class)`, overrides both. If nothing matches, a `LogicException` reports that no resource class was found. Laravel 13 also adds a `--json-api` flag to `make:resource` for its first-party JSON:API resources, a separate, spec-driven variant. ## Why not return the model directly Returning `$workout` from a route also produces JSON, via the model's own serialization (its `toArray()`, filtered by the model's hidden and visible lists). The difference is where the response contract is defined. | Concern | Returning the model | Returning a resource | |---|---|---| | Which fields appear | Every column and appended accessor not hidden on the model | Only the keys listed in `toArray()` | | A new column in a migration | Appears in the API on the next deploy | Invisible until someone adds it to the resource | | Renaming or formatting | Needs accessors on the model, affecting every use | Done per API shape, model untouched | | Conditional fields and relations | Not expressible per request | `when()`, `whenLoaded()`, `whenCounted()` | | Top-level wrapper and meta | None; bare object or paginator array | `data` wrapper, `with()`, `additional()`, pagination `links` and `meta` | For a public mobile API this is decisive: shipped app versions cannot be updated instantly, so the JSON shape is a contract, and a contract should be an explicit **allow-list** reviewed in one file rather than a side effect of the table schema. Hiding sensitive attributes on the model is a second, model-wide safety net; it is not a substitute for choosing what each endpoint exposes. Resources also keep presentation choices, such as ISO 8601 dates, rounded units and renamed keys, out of the model, so a web page, a queued export and the mobile API can each present the same model differently. ## A minimal example ```php <?php namespace App\Http\Resources; use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\JsonResource; class WorkoutResource extends JsonResource { public function toArray(Request $request): array { return [ 'id' => $this->id, 'type' => $this->type, 'distance_km' => round($this->distance_m / 1000, 2), 'duration_s' => $this->duration_s, 'started_at' => $this->created_at?->toIso8601String(), ]; } } ``` The `device_serial` and `user_id` columns never reach the client, the unit conversion lives in one place, and the model stays free of API-specific accessors. ## Common traps - **Leaving the stub untouched** and believing the resource hides anything; it returns the model's full array. - **Reading relations directly** (`$this->user->name`) inside a resource used for a list, which lazy-loads one query per item; use `whenLoaded()` and eager-load in the controller. - **Calling `->toArray()` on the resource yourself** and returning that array, which skips the wrapper, `with()`, `additional()` and status handling that `toResponse()` provides. - **Assuming the resource changes persistence**: it is output only. Saving, mass assignment and validation happen elsewhere.
- What does the toArray() in a freshly generated make:resource stub return?It returns `parent::toArray($request)`, and `JsonResource::toArray()` returns the wrapped model's own `toArray()` (or the array itself if you wrapped an array, or `[]` for null). So an untouched resource outputs every visible attribute and appended accessor of the model; it only becomes an allow-list once you replace that line with explicit keys.
- How does $this->distance_m resolve inside a resource that declares no such property?`JsonResource` uses the `DelegatesToResource` trait. Its `__get` reads `$this->resource->{$key}`, `__isset` checks the same, and `__call` forwards method calls (after checking resource macros) to the wrapped model. Array access is forwarded too, so a resource wrapping an array also works with `$this['key']`.
- What happens when $workout->toResource() cannot find a matching class?It first honours a `#[UseResource]` attribute on the model, then tries `App\Http\Resources\WorkoutResource` and `App\Http\Resources\Workout`. If none of those classes exists it throws a `LogicException` saying it failed to find a resource class for the model. It never generates a class on the fly; pass a class name to `toResource()` or add the attribute.
saying these in an interview costs you the question
- The make:resource stub already exposes only safe, whitelisted fields.
- A resource changes how the model is stored or validated.
- $this->title fails inside a resource because JsonResource has no title property.
- Returning the model is just as safe, because new columns never reach the JSON.
- toResource() generates a resource class at runtime when none exists.
- Resources are only for collections; single models should be returned bare.