Tulip Logo IconTulip
Query

Pagination

Use query pagination strategies for URL-backed page and limit state.

Pagination strategies define the URL state, API schema, table state mapping, and reset behavior for paginated queries.

Offset Pagination

The built-in offset strategy uses zero-based pages.

import { offsetPagination } from "@tulip-systems/query";
import { usePagination } from "@tulip-systems/query/client";

const pagination = usePagination(offsetPagination);

The controller exposes:

{
  strategy;
  input;
  tableState;
  onPaginationChange;
}

pagination.input is the API shape:

type OffsetPaginationInput = {
  page: number;
  limit: number;
};

Use page, not cursor, for shared query and table APIs.

API Schemas

Use offsetPaginationSchema in route inputs.

import { offsetPaginationSchema } from "@tulip-systems/query";
import z from "zod";

export const listProjectsInputSchema = z.object({
  pagination: offsetPaginationSchema,
  filters: projectFiltersSchema,
  sorts: projectSortsSchema,
});

Database Offset

Convert page and limit to SQL offset at the backend boundary.

import { parseOffsetPagination } from "@tulip-systems/query";

const { limit, offset } = parseOffsetPagination(input.pagination);

Response DTO

Use offsetPaginationResponse(...) to create the standard paginated response.

import { offsetPaginationResponse } from "@tulip-systems/query";

return offsetPaginationResponse({
  data,
  pagination: input.pagination,
  total,
});

The response shape is:

type OffsetPaginatedResponse<TData> = {
  data: TData[];
  pagination: {
    total: number;
    totalPages: number;
    limit: number;
    hasNextPage: boolean;
    hasPreviousPage: boolean;
    page: number;
    nextPage: number | null;
    previousPage: number | null;
  };
};

Infinite Queries

Infinite queries still use page numbers as pageParam.

const response = useSuspenseInfiniteQuery(
  orpc.projects.list.infiniteOptions({
    initialPageParam: 0,
    input: (page: number) => ({
      ...input,
      pagination: { ...input.pagination, page },
    }),
    getNextPageParam: ({ pagination }) => pagination.nextPage,
    getPreviousPageParam: ({ pagination }) => pagination.previousPage,
  }),
);

Keep provider-native cursors or opaque page tokens inside provider query code. The shared dataset and table API should expose page and limit values.

On this page