Your Vue SPA calls /api/posts, logs the response, and the list is somehow on response.data.data. Or worse, a field you never meant to publish is in the JSON. API resources fix both: a fixed allow-list of keys and a predictable envelope.
TL;DR
- A resource is the allow-list of keys an HTTP client sees. Inertia pages can keep passing models.
PostResource::collection($paginator)returns{ data, links, meta }.- In Axios,
response.data.datais the list: Axios envelope, then Laravel’s. - Never put private file URLs or whole related models in a resource.
An Inertia controller can return the Post model. The page receives props, not a JSON document you designed. A resource is for the other client: a Vue SPA or Nuxt that calls /api/posts and has to know the keys. That split is Inertia, a SPA, or Nuxt. The cookie on that request is Sanctum. The Nuxt call is Nuxt in front of the API.
One post#
php artisan make:resource PostResourcepublic function toArray(Request $request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'excerpt' => $this->excerpt,
'published_at' => $this->published_at,
];
}Those four keys match the Post from the setup guide. Add a key here when the client needs it.
Leaks whatever the model has, then Only the keys you chose:
return Post::findOrFail($id);return new PostResource(Post::findOrFail($id));Gotcha: the resource is the allow-list
Do not return the whole model and hope Vue ignores remember_token style extras later. A resource is the allow-list.
A page of posts#
PostResource::collection on a paginator keeps Laravel’s links and meta and wraps each row.
Route::get('/posts', function () {
$posts = Post::query()->latest('published_at')->paginate(10);
return PostResource::collection($posts);
})->middleware('auth:sanctum');The body is { data, links, meta }. data is the ten posts. links.next and meta.current_page are the paginator. Vue code that treats the body as an array will render nothing.
const response = await axios.get("/api/posts");
const posts = response.data.data;Tip: the double data is expected
response.data is the HTTP body. response.data.data is the list. The double data is Laravel’s resource envelope, then Axios’s envelope. It is not a bug in either one.
meta.last_page is present because this example uses paginate(10). Pagination is what changes if you switch to simplePaginate or cursorPaginate. The resource does not add a total those paginators refused to count.
Pin the envelope in a test
it('returns a paginated resource', function () {
Post::factory()->count(3)->create();
$this->actingAs(User::factory()->create())
->getJson('/api/posts')
->assertOk()
->assertJsonStructure(['data' => [['id', 'title', 'excerpt', 'published_at']], 'links', 'meta']);
});What not to put in the resource#
Do not call Storage::url for a private file and hand the URL to any logged-in user. Public covers from the upload guide can be a /storage/... path. Anything else stays behind a controller that checks the user.
Do not embed the entire author model. Pass author: $this->user?->only(['id', 'name']) when you add that relation. Until the post has a user, leave it out. The policy guide is what adds user_id.
Where this stops#
Heads up: one source per list
Inertia pages should not fetch this endpoint to paint the list they already receive as props. Two sources for the same rows will drift. Use the resource when the client is actually an HTTP client.
Recap#
- Use a resource when the client is a real HTTP client.
toArrayis the allow-list; add keys deliberately.- A paginated collection keeps
linksandmeta; rows are ondata. - Private files and full related models stay out.
FAQ#
Why is my Laravel data on response.data.data in Vue?
response.data is the Axios body; Laravel resources wrap rows in a data key. Both are by design.
Do I need API resources with Inertia?
Usually not. Inertia controllers pass props to the page. Use a resource when a separate SPA, Nuxt or a mobile app calls the endpoint.
Can I remove the data wrapper from Laravel resources?
JsonResource::withoutWrapping() removes it for single resources, but paginated collections still return data, links and meta. Most Vue clients just read data.



