Pagination
Pagination support is available only for useAsyncData composables. useFetch composables do not support pagination.
How it works
When you call a composable with paginated: true, the wrapper automatically:
- Injects
pageandperPageparameters into every request (as query params, body, or headers) - After each response, reads
total,totalPages,currentPageandperPagefrom the response - Exposes reactive pagination state and navigation helpers
Setup: the plugin
The pagination convention is defined once in a Nuxt plugin. The plugin is generated by nxh generate at plugins/api-pagination.ts and is never overwritten — your changes are safe.
Open it and uncomment the example that matches your backend:
// plugins/api-pagination.ts
export default defineNuxtPlugin(() => {
const paginationConfig = {
meta: {
metaSource: 'body',
fields: {
total: 'total',
totalPages: 'totalPages',
currentPage: 'currentPage',
perPage: 'perPage',
dataKey: 'data', // the array lives in response.data
},
},
request: {
sendAs: 'query',
params: { page: 'page', perPage: 'limit' },
defaults: { page: 1, perPage: 20 },
},
}
return {
provide: {
getGlobalApiPagination: () => paginationConfig,
},
}
})Plugin reference
meta — reading the response
Controls how the wrapper reads pagination data from the backend response.
| Field | Type | Description |
|---|---|---|
metaSource | 'body' | 'headers' | Where to read pagination metadata from |
fields.total | string | Field name for total item count |
fields.totalPages | string | Field name for total page count |
fields.currentPage | string | Field name for current page number |
fields.perPage | string | Field name for page size |
fields.dataKey | string? | If the items array is nested (e.g. response.data), set this key. Leave undefined if the response root is the array. |
For metaSource: 'body', field names support dot-notation: 'meta.total', 'pagination.lastPage'.
For metaSource: 'headers', field names are HTTP header names: 'X-Total-Count'.
⚠️ Headers mode requires the Raw composable.
StandarduseAsyncDatacomposables do not expose HTTP response headers. To read pagination metadata from headers you must use theRawvariant (e.g.useAsyncDataGetPetsRaw), which gives the wrapper access to the full response object.
request — sending page params
Controls how the wrapper injects page and perPage into outgoing requests.
| Field | Type | Description |
|---|---|---|
sendAs | 'query' | 'body' | 'headers' | Where to attach the pagination params |
params.page | string | Query/body/header key for the page number |
params.perPage | string | Query/body/header key for the page size |
defaults.page | number | Starting page (default: 1) |
defaults.perPage | number | Default page size (default: 20) |
Common backend examples
Standard REST (body meta)
GET /pets?page=1&limit=20
→ { data: [...], total: 100, totalPages: 5, currentPage: 1, perPage: 20 }const paginationConfig = {
meta: {
metaSource: 'body',
fields: {
total: 'total',
totalPages: 'totalPages',
currentPage: 'currentPage',
perPage: 'perPage',
dataKey: 'data',
},
},
request: {
sendAs: 'query',
params: { page: 'page', perPage: 'limit' },
defaults: { page: 1, perPage: 20 },
},
}Laravel paginate()
GET /pets?page=1&per_page=15
→ { data: [...], meta: { total: 100, last_page: 7, current_page: 1, per_page: 15 } }const paginationConfig = {
meta: {
metaSource: 'body',
fields: {
total: 'meta.total',
totalPages: 'meta.last_page',
currentPage: 'meta.current_page',
perPage: 'meta.per_page',
dataKey: 'data',
},
},
request: {
sendAs: 'query',
params: { page: 'page', perPage: 'per_page' },
defaults: { page: 1, perPage: 15 },
},
}HTTP response headers (Raw only)
GET /pets?page=1&limit=20
← X-Total-Count: 100
← X-Total-Pages: 5
← X-Page: 1
← X-Per-Page: 20
Response body: [...]const paginationConfig = {
meta: {
metaSource: 'headers', // ← requires Raw composable
fields: {
total: 'X-Total-Count',
totalPages: 'X-Total-Pages',
currentPage: 'X-Page',
perPage: 'X-Per-Page',
// no dataKey — body is the array directly
},
},
request: {
sendAs: 'query',
params: { page: 'page', perPage: 'limit' },
defaults: { page: 1, perPage: 20 },
},
}POST-as-search (body pagination)
POST /pets/search { filters: {...}, page: 1, pageSize: 20 }
→ { items: [...], total: 100, pages: 5 }const paginationConfig = {
meta: {
metaSource: 'body',
fields: {
total: 'total',
totalPages: 'pages',
currentPage: 'page',
perPage: 'pageSize',
dataKey: 'items',
},
},
request: {
sendAs: 'body',
params: { page: 'page', perPage: 'pageSize' },
defaults: { page: 1, perPage: 20 },
},
}Using pagination in a component
Enable pagination by passing paginated: true to the composable:
const { data, pagination, pending } =
useAsyncDataFindPets({}, { paginated: true })pagination — reactive state and controls
All pagination state and navigation helpers are accessed through pagination.value:
// State
pagination.value.currentPage // 1
pagination.value.totalPages // 5
pagination.value.total // 100
pagination.value.perPage // 20
pagination.value.hasNextPage // true
pagination.value.hasPrevPage // false
// Navigation — each call automatically triggers a new fetch
pagination.value.nextPage() // go to page + 1
pagination.value.prevPage() // go to page - 1
pagination.value.goToPage(3) // jump to a specific page
pagination.value.setPerPage(50) // change page size and reset to page 1Full example
<script setup>
const { data: pets, pagination, pending } =
useAsyncDataFindPets({}, { paginated: true })
</script>
<template>
<div v-if="pending">Loading...</div>
<ul v-else>
<li v-for="pet in pets" :key="pet.id">{{ pet.name }}</li>
</ul>
<div>
<button :disabled="!pagination.hasPrevPage" @click="pagination.prevPage">← Prev</button>
<span>Page {{ pagination.currentPage }} of {{ pagination.totalPages }}</span>
<button :disabled="!pagination.hasNextPage" @click="pagination.nextPage">Next →</button>
</div>
</template>Overriding pagination per composable call
The global plugin config applies to every paginated call. You can override it for a single call using paginationConfig:
// This call uses a different page size key than the global config
const { data } = useAsyncDataGetProducts({}, {
paginated: true,
paginationConfig: {
meta: {
metaSource: 'body',
fields: {
total: 'count',
totalPages: 'pages',
currentPage: 'page',
perPage: 'size',
dataKey: 'results',
},
},
request: {
sendAs: 'query',
params: { page: 'page', perPage: 'size' },
defaults: { page: 1, perPage: 10 },
},
},
})Priority order: per-call paginationConfig > plugin global config > built-in defaults.
Starting on a specific page
const { data, pagination } = useAsyncDataFindPets({}, {
paginated: true,
initialPage: 2,
initialPerPage: 50,
})