Skip to content

Pagination & Cursor Quick Recipes ​

Use these recipes to quickly configure how pagination and cursor output is emitted.

1. Default behavior ​

ts
import { defineConfig } from 'resora';

export default defineConfig({
  paginatedExtras: ['meta', 'links'],
  baseUrl: '',
  pageName: 'page',
});

When pagination.path is available on a collection resource, links are generated as path-relative URLs. If a baseUrl is configured or the request URL is auto-detected from context, links become absolute.

Example:

json
{
  "data": [{ "id": 1 }],
  "meta": {
    "current_page": 2,
    "last_page": 10,
    "path": "/users"
  },
  "links": {
    "prev": "/users?page=1",
    "next": "/users?page=3"
  }
}

2. Custom API domain and page query name ​

ts
import { defineConfig } from 'resora';

export default defineConfig({
  baseUrl: 'https://api.example.com/v1',
  pageName: 'p',
});

Generated links become:

json
{
  "links": {
    "first": "https://api.example.com/v1/users?p=1",
    "next": "https://api.example.com/v1/users?p=3"
  }
}

3. Emit cursor as its own root block ​

ts
import { defineConfig } from 'resora';

export default defineConfig({
  paginatedExtras: {
    meta: 'meta',
    links: 'links',
    cursor: 'cursor',
  },
});

Cursor output:

json
{
  "data": [{ "id": 1 }],
  "cursor": {
    "previous": "cursor_prev",
    "next": "cursor_next"
  }
}

4. Rename cursor keys with cursorMeta ​

ts
import { defineConfig } from 'resora';

export default defineConfig({
  paginatedExtras: {
    cursor: 'cursor_info',
  },
  cursorMeta: {
    previous: 'before',
    next: 'after',
  },
});

Cursor output:

json
{
  "cursor_info": {
    "before": "cursor_prev",
    "after": "cursor_next"
  }
}
ts
import { defineConfig } from 'resora';

export default defineConfig({
  paginatedMeta: {
    current_page: 'currentPage',
    last_page: 'lastPage',
    per_page: 'perPage',
    total: 'totalItems',
    path: 'endpoint',
  },
  paginatedLinks: {
    prev: 'previous',
    next: 'next',
    first: 'first',
    last: 'last',
  },
});

Example output:

json
{
  "meta": {
    "currentPage": 2,
    "lastPage": 10,
    "perPage": 25,
    "totalItems": 250,
    "endpoint": "/users"
  },
  "links": {
    "previous": "/users?page=1",
    "next": "/users?page=3"
  }
}

6. Use request page params as Arkorm defaults ​

When Arkorm pagination runs before you build the response, wire Resora's request URL parsing into Arkorm's runtime resolver.

ts
import { Resource, createArkormCurrentPageResolver } from 'resora';
import { configureArkormRuntime } from 'arkormx';

app.use((req, _res, next) => {
  Resource.setCtx({ req });
  next();
});

configureArkormRuntime(() => prisma, {
  pagination: {
    resolveCurrentPage: createArkormCurrentPageResolver(),
  },
});

const users = await User.query().paginate(15, undefined, {
  pageName: 'p',
});

With a request like /users?p=3, Arkorm will default to page 3 and Resora will generate links using the same pageName.

Notes ​

  • Pagination links are generated from numeric page values (firstPage, lastPage, prevPage, nextPage) and pagination.path.
  • If paginatedExtras.cursor is not configured, cursor values are emitted under meta.cursor by default.
  • Set baseUrl to your public API origin for absolute URLs in production, or use URL auto-detection to derive paths from the request context.

7. Automatic URL detection from request context ​

When your pagination data does not include an explicit path, Resora can automatically detect the current request URL from the HTTP context and use it for link generation — including any existing query string parameters.

This works with both Express and H3 because both expose a { req, res } context shape.

Passing context to the constructor ​

Pass the full { req, res } context (or the framework event) as the second argument:

ts
app.get('/api/users', async (req, res) => {
  const users = await getUsers(req.query);

  // Pass { req, res } — Resora reads req.originalUrl for the path
  return await new ResourceCollection(users, { req, res });
});
ts
export default defineEventHandler((event) => {
  const users = await getUsers(getQuery(event));

  // Pass the H3 event — Resora reads event.req.url
  return new ResourceCollection(users, event);
});

If the user navigates to /api/users?search=foo&sort=name, the generated links will preserve the query string:

json
{
  "links": {
    "first": "/api/users?search=foo&sort=name&page=1",
    "next": "/api/users?search=foo&sort=name&page=2"
  }
}

With baseUrl configured, the links become full URLs:

json
{
  "links": {
    "first": "https://api.example.com/api/users?search=foo&sort=name&page=1",
    "next": "https://api.example.com/api/users?search=foo&sort=name&page=2"
  }
}

Priority

An explicit pagination.path always takes precedence over the auto-detected URL. If both are present, the explicit path wins.

Using setCtx() in middleware ​

If you prefer to set the request context once in a middleware (instead of passing it to each resource), use the static setCtx() method available on all serializer classes:

ts
import { Resource } from 'resora';

// Register middleware before your routes
app.use((req, res, next) => {
  Resource.setCtx({ req, res });
  next();
});

// In your route handler — no need to pass context
app.get('/api/users', async (req, res) => {
  const users = await getUsers(req.query);
  return await new ResourceCollection(users, res);
});
ts
import { Resource } from 'resora';

app.use((event) => {
  Resource.setCtx(event);
});

export default defineEventHandler((event) => {
  return new ResourceCollection(users);
});

setCtx() is inherited by Resource, ResourceCollection, and GenericResource — calling it on any one of them makes the request URL available for all of them.

Request scoping

setCtx() stores the URL globally. If your application handles concurrent requests in a shared process, call setCtx() at the start of each request to ensure accuracy. For single-request-per-process runtimes (serverless, edge workers), this is automatic.

How URL extraction works ​

Resora inspects the context using the common { req, res } interface:

FrameworkRequest URL sourceExample value
Expressreq.originalUrl/api/users?search=foo
H3req.url (Web standard Request)http://localhost:3000/api/users?q=1

For H3's Web standard Request, the full URL is parsed and only the pathname + search is used for link generation.

If a bare response object is passed (without req), URL detection is skipped and the resource falls back to pagination.path or the bare /?page=X format.