# Laravel API resources for a Vue client

> A resource is the JSON shape a SPA or Nuxt reads. An Inertia page can keep passing the model. Axios sees the rows on response.data.data.

- URL: https://laravel-vuejs.space/laravel-api-resources-vue
- Author: Anass Ez-zouaine (https://laravel-vuejs.space/author/anass-ez-zouaine)
- Published: 2026-10-10
- Topics: Laravel, VueJs

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.data` is the list: Axios envelope, then Laravel’s.
> -   Never put private file URLs or whole related models in a resource.
>
> **What you'll learn**
>
> -   When you need a resource (and when Inertia does not)
> -   How to shape one post and a paginated list
> -   Why Axios rows are on `response.data.data`
>
> **Who it's for:** Developers serving JSON to a Vue SPA or Nuxt
>
> **Applies to:** Laravel 13 · Sanctum · Vue 3 · Axios

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](https://laravel-vuejs.space/laravel-vue-in-2026). The cookie on that request is [Sanctum](https://laravel-vuejs.space/laravel-sanctum-vue-spa). The Nuxt call is [Nuxt in front of the API](https://laravel-vuejs.space/nuxt-laravel-api).

## One post

```bash
php artisan make:resource PostResource
```

`app/Http/Resources/PostResource.php`
```php
public 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](https://laravel-vuejs.space/application-setup). Add a key here when the client needs it.

**Leaks whatever the model has:**

`routes/api.php`
```php
return Post::findOrFail($id);
```

**Only the keys you chose:**

`routes/api.php`
```php
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.

`routes/api.php`
```php
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.

```ts
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](https://laravel-vuejs.space/laravel-vue-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**
>
> `tests/Feature/PostsApiTest.php`
> ```php
> 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](https://laravel-vuejs.space/laravel-vue-file-upload) 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](https://laravel-vuejs.space/laravel-vue-policies) 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.
- `toArray` is the allow-list; add keys deliberately.
- A paginated collection keeps `links` and `meta`; rows are on `data`.
- Private files and full related models stay out.

## FAQ

**Q: 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.

**Q: 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.

**Q: 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`.

## Next steps

- [Laravel and Vue pagination](https://laravel-vuejs.space/laravel-vue-pagination)
- [Nuxt in front of a Laravel API](https://laravel-vuejs.space/nuxt-laravel-api)
- [Sanctum for a Vue SPA](https://laravel-vuejs.space/laravel-sanctum-vue-spa)

## Sources

- [Laravel 13: Eloquent API resources](https://laravel.com/docs/13.x/eloquent-resources)
- [Axios: Response schema](https://axios-http.com/docs/res_schema)
