Pagination

@websideproject/nuxt-auto-api supports offset (page) pagination and cursor (keyset) pagination. Every list is capped: ?limit defaults to pagination.defaultLimit (20) and can never exceed pagination.maxLimit (100).

Pagination

@websideproject/nuxt-auto-api supports offset (page) pagination and cursor (keyset) pagination. Every list is capped: ?limit defaults to pagination.defaultLimit (20) and can never exceed pagination.maxLimit (100).

Offset pagination

GET /api/users?page=2&limit=20&sort=-createdAt
{
  "data": [...],
  "meta": { "page": 2, "limit": 20, "total": 156 }
}

total counts exactly the rows the caller may see (tenant, listFilter, soft delete, filters). When the resource has an objectLevel check the SQL count cannot know which rows the function drops, so total is omitted rather than wrong, and hasMore says whether another page follows:

{
  "data": [...],
  "meta": { "page": 2, "limit": 20, "hasMore": true }
}

Pages still hold exactly the rows the caller may see, each row on one page. Under objectLevel, page N checks every row before it, so deep pages cost more; use cursor pagination for long lists of such a resource.

Cursor pagination

Stable for infinite scroll and large tables: rows inserted or deleted between requests never shift a page.

GET /api/users?limit=20&sort=-createdAt&cursor=         ← first page: an EMPTY cursor
GET /api/users?limit=20&sort=-createdAt&cursor=<nextCursor>
{
  "data": [...],
  "meta": { "limit": 20, "hasMore": true, "nextCursor": "WzE3MDAwMDAwMDAwMCwxNDNd" }
}

Continue until hasMore is false. Send cursor= (empty) on the first request — without a cursor parameter the list is paginated by offset and has no nextCursor.

How it works

The page order is your sort plus the primary key as a tiebreaker, so the order is total and a page boundary is never ambiguous — even when many rows share a createdAt. The cursor holds the last row's values for those columns, and the next page is every row strictly after it:

sort=-createdAt  →  ORDER BY created_at DESC, id DESC
next page        →  WHERE created_at < ?  OR (created_at = ? AND id < ?)
  • Cursors follow the sort — keep the same sort (and filter) while paging.
  • Columns used for sorting with cursors must be NOT NULL; a cursor carrying a NULL is a 400.
  • A tampered or malformed cursor is a 400.

Filtering

?filter= is a JSON object; its conditions are ANDed:

GET /api/posts?filter={"status":"published","views":{"$gte":10,"$lte":100}}
FormMeaning
{ "field": value }equals (null → IS NULL, an array → IN)
$eq $ne $gt $gte $lt $ltecomparisons
$likecontains (LIKE '%value%')
$in $ninin / not in a list (an array or a comma list, at most 500 values; 50 on D1)
$nulltrue → IS NULL, false → IS NOT NULL
{ "$or": [filter, …] }any of several filters — top level only, at most 20
GET /api/posts?filter={"published":true,"$or":[{"title":{"$like":"nuxt"}},{"body":{"$like":"nuxt"}}]}

On a timestamp column the values are dates: an ISO string or epoch milliseconds ({"createdAt":{"$gte":"2026-01-01"}}).

Only columns the caller may read can be filtered: an unknown, hidden or unreadable field, an unknown operator or a value that is not a date where one is needed is a 400 — a condition is never silently dropped, because that would widen the result.

Sorting

GET /api/posts?sort=-publishedAt,title

A comma list (or repeated sort), - for descending. Only columns the caller may read can be used; an unknown or hidden column is a 400.

Configuration

// nuxt.config.ts
export default defineNuxtConfig({
  autoApi: {
    pagination: {
      defaultLimit: 20,
      maxLimit: 100, // also caps ?include pagination and M2M lists
    },
  },
})

Frontend usage

Offset:

<script setup lang="ts">
const page = ref(1)
const { data } = useAutoApiList('users', computed(() => ({ page: page.value, limit: 20, sort: '-createdAt' })))
</script>

Cursor (infinite scroll) — useAutoApiInfinite sends the empty first cursor for you:

<script setup lang="ts">
const { data, fetchNextPage, hasNextPage } = useAutoApiInfinite('users', { limit: 20, sort: '-createdAt' })
</script>

Best practices

  • Offset for UIs with page numbers; cursor for infinite scroll, feeds and large tables.
  • Index the columns you sort by, together with the primary key (e.g. (created_at, id)).
  • Don't raise maxLimit to fetch "everything" — page through it.

Need a Landing Page?

Modern landing pages with optional modules (blog, docs, forms, i18n). Let's discuss your project.

Build Your MVP

Full-stack SaaS development. Expert in database design, multi-tenancy, and scalable architecture.

Deployment Help

Dockerize your backend, set up CI/CD pipelines, deploy to Cloudflare or Hetzner. Early-stage setup.

Suggest a SaaS Tool

Missing a calculator or tool? Suggest what you'd like to see on our site.