Skip to content

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:

  1. Injects page and perPage parameters into every request (as query params, body, or headers)
  2. After each response, reads total, totalPages, currentPage and perPage from the response
  3. 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:

ts
// 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.

FieldTypeDescription
metaSource'body' | 'headers'Where to read pagination metadata from
fields.totalstringField name for total item count
fields.totalPagesstringField name for total page count
fields.currentPagestringField name for current page number
fields.perPagestringField name for page size
fields.dataKeystring?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.
Standard useAsyncData composables do not expose HTTP response headers. To read pagination metadata from headers you must use the Raw variant (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.

FieldTypeDescription
sendAs'query' | 'body' | 'headers'Where to attach the pagination params
params.pagestringQuery/body/header key for the page number
params.perPagestringQuery/body/header key for the page size
defaults.pagenumberStarting page (default: 1)
defaults.perPagenumberDefault 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 }
ts
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 } }
ts
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: [...]
ts
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 }
ts
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:

ts
const { data, pagination, pending } =
  useAsyncDataFindPets({}, { paginated: true })

pagination — reactive state and controls

All pagination state and navigation helpers are accessed through pagination.value:

ts
// 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 1

Full example

vue
<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:

ts
// 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

ts
const { data, pagination } = useAsyncDataFindPets({}, {
  paginated: true,
  initialPage: 2,
  initialPerPage: 50,
})

Released under the Apache-2.0 License.