---
name: laravel-api-resources
description: >-
  Laravel API Resource design: decoupling API shape from database shape,
  pagination by default, conditional fields with when()/whenLoaded(),
  versioning breaking changes, and consistent error responses. Use when
  building or reviewing API endpoints.
argument-hint: "[endpoint or resource]"
license: MIT
metadata:
  author: John Felix Lim
  version: "1.0.0"
---
# API Resource Design

Apply when adding or reviewing an API endpoint. Before returning a model from a controller, check whether a Resource already exists for it — if not, that's the first thing to add, not an optional polish step.

## Rules

1. **API Resources are the seam between your database shape and your API shape** — A model's columns are an implementation detail; a Resource is the actual contract with consumers, and the two are allowed to diverge.
2. **Version the contract, not just the URL** — A breaking change to a Resource's shape needs a new version consumers can opt into. Silently changing `UserResource`'s fields under the same endpoint breaks every existing client at once.
3. **Paginate by default, not as an optimization added later** — An unpaginated `index` endpoint is a resource-exhaustion bug waiting for the table to grow. Paginate from the first version, even when the table is small today.
4. **Conditional fields belong in the Resource, not scattered across controllers** — `$this->when()` keeps the 'does this field show for this viewer' logic in one place instead of duplicated per endpoint that happens to return the same model.
5. **Errors need a consistent shape across every endpoint** — A client that has to branch on which endpoint it called to know how to parse an error response is a sign the API has no actual error contract.

## Patterns

### Leaking the database shape directly

**Avoid** — The API's shape is whatever the migrations happen to produce today, and it changes the moment a column is added or renamed.

```php
// Controller returns the model directly.
public function show(Post $post)
{
    return $post; // every column, every relation lazy-loaded on access,
                   // and any column added later ships automatically.
}
```

**Prefer** — The API contract is explicit and stable — a new database column doesn't change the response until someone deliberately adds it to the Resource.

```php
class PostResource extends JsonResource
{
    public function toArray($request)
    {
        return [
            'id' => $this->id,
            'title' => $this->title,
            'excerpt' => $this->excerpt,
            'author' => new AuthorResource($this->whenLoaded('author')),
        ];
    }
}

public function show(Post $post)
{
    return new PostResource($post->load('author'));
}
```

### Unpaginated index endpoint

**Avoid** — Works fine at 200 rows, times out or exhausts memory at 200,000 — and by then it's a production incident, not a code review comment.

```php
public function index()
{
    return PostResource::collection(Post::all()); // all of them
}
```

**Prefer** — Pagination is part of the contract from day one, so growth in data never becomes a breaking performance change.

```php
public function index(Request $request)
{
    return PostResource::collection(
        Post::query()->paginate($request->integer('per_page', 25)),
    );
}
```

### Conditional fields duplicated per controller

**Avoid** — The visibility rule lives wherever a developer remembered to copy it, and drifts the first time only two of three copies get updated.

```php
// Same "is this the owner" check re-implemented in three
// controllers that each happen to return a Post.
return [
    'id' => $post->id,
    'title' => $post->title,
    'draft_notes' => $request->user()?->id === $post->author_id
        ? $post->draft_notes
        : null,
];
```

**Prefer** — The visibility rule lives in exactly one place, and every endpoint returning a PostResource inherits it automatically.

```php
class PostResource extends JsonResource
{
    public function toArray($request)
    {
        return [
            'id' => $this->id,
            'title' => $this->title,
            'draft_notes' => $this->when(
                $request->user()?->id === $this->author_id,
                $this->draft_notes,
            ),
        ];
    }
}
```

## Review checklist

- [ ] No controller returns a raw model or `Model::all()` result directly.
- [ ] Every list endpoint paginates, even ones that feel small today.
- [ ] Conditional/visibility logic for fields lives in the Resource via `when()`/`whenLoaded()`, not duplicated in controllers.
- [ ] Breaking changes to a Resource's shape ship as a new version, not a silent change to the existing one.
- [ ] Error responses share one consistent shape (status, message, error code) across every endpoint.
- [ ] Eager loading needed by a Resource is loaded by the controller before the Resource runs, not triggered lazily per-request.

---

From the John Felix Lim Frontend Engineering Playbook — https://github.com/JohnFelixLim