How CommonGrants Handles Pagination, Filtering, and Sorting: A Technical Guide

CommonGrants implements a unified, type-safe protocol for list operations using Zod schemas for pagination, filtering, and sorting parameters, with automatic client-side pagination handling via the TypeScript SDK.

The hhs/simpler-grants-protocol repository defines a standardized approach to requesting lists of resources—such as funding opportunities—through its TypeScript SDK located in lib/ts-sdk/src. This system ensures that every list endpoint supports pagination, filtering, and sorting through composable Zod schemas and consistent response wrappers.

Core Schemas for Pagination, Filtering, and Sorting

The foundation of CommonGrants list operations resides in four schema files under lib/ts-sdk/src/schemas/zod/. These define the contract for request parameters and response metadata.

Pagination Schemas

The pagination.ts file exports PaginatedQueryParamsSchema and PaginatedBodyParamsSchema, which validate that page is an integer ≥ 1 and pageSize is an integer ≥ 1 with a default of 100. The response metadata is defined by PaginatedResultsInfoSchema, which returns currentPage, pageSize, totalItems, and totalPages in every paginated response.

Sorting Schemas

Defined in sorting.ts, the SortQueryParamsSchema and SortBodyParamsSchema accept sortBy (any field name), an optional customSortBy key for implementation-specific sorting, and sortOrder constrained to asc or desc. The SortedResultsInfoSchema returns the applied sortBy, customSortBy, sortOrder, and any non-fatal sorting errors in the sortInfo response field.

Filtering Schemas

The filters.ts file provides Zod definitions for all supported filter operators through enums like EquivalenceOperatorsEnum and ComparisonOperatorsEnum. It exports generic filter schemas such as DefaultFilterSchema and StringComparisonFilterSchema, allowing servers to validate complex filter objects containing equality checks, range comparisons, array inclusion, and string pattern matching.

Response Wrappers

The responses.ts file composes the individual concerns into higher-order schemas. PaginatedSchema, SortedSchema, and FilteredSchema wrap any item schema to produce a standardized response shape. For example, FilteredSchema extends SortedSchema to add a filterInfo object containing the applied filters and any validation errors:

export const FilteredSchema = <ItemsT extends z.ZodTypeAny, FilterT extends z.ZodTypeAny>(
  itemsSchema: ItemsT,
  filterSchema: FilterT
) =>
  SortedSchema(itemsSchema).extend({
    filterInfo: z.object({
      filters: filterSchema,
      errors: z.array(z.string()).nullish(),
    }).strict(),
  });

(see lib/ts-sdk/src/schemas/zod/responses.ts lines 30‑40)

Client-Side Implementation

The TypeScript SDK provides both low-level HTTP utilities and high-level resource clients to handle pagination, filtering, and sorting automatically.

Low-Level HTTP Client

The Client class in lib/ts-sdk/src/client/client.ts provides the foundation for all API interactions. It exposes get and post methods for basic requests, but the key feature for pagination is fetchMany. This method implements auto-pagination by repeatedly calling fetchOnePage until the requested maxItems limit is reached or the server indicates the last page via isLastPage.

The fetchMany logic (lines 14‑49) handles the loop, while fetchOnePage (lines 60‑82) attaches the pagination object to the request body for POST requests or to query parameters for GET requests.

Resource-Specific Client

The OpportunityClient in lib/ts-sdk/src/client/opportunities.ts demonstrates practical usage of the protocol. The searchOpportunities method constructs a request that may include:

  • Filters: Built via buildSearchBody (lines 74‑93), which converts shorthand parameters like statuses into formal filter objects.
  • Sorting: Supplied by the caller or defaulting to lastModifiedAt descending.
  • Pagination: Injected automatically by fetchSearchPage (lines 98‑110) if not provided.

The method returns a fully typed result parsed with FilteredSchema, containing items, paginationInfo, sortInfo, and filterInfo.

Request Flow: GET vs POST

CommonGrants supports both HTTP methods for list operations, with schemas adapting to each transport style.

Step GET (e.g., /common-grants/opportunities) POST (e.g., /common-grants/opportunities/search)
Client prepares request client.get(path, { params: { page, pageSize, … } }) adds query string parameters. client.post(path, { …, pagination: { page, pageSize } }) embeds parameters in the JSON body.
Server validation PaginatedQueryParamsSchema validates query parameters. PaginatedBodyParamsSchema validates the pagination object inside the JSON body.
Filtering Optional filters may be passed as query parameters, though most implementations embed filters in POST bodies. filters are part of the JSON body, validated against schemas from filters.ts.
Sorting SortQueryParamsSchema validates sortBy, customSortBy, and sortOrder in the query string. SortBodyParamsSchema validates the same fields inside the request body.
Response PaginatedSchema composes the final JSON, always including paginationInfo. If sorting/filtering were requested, sortInfo and filterInfo are also present. Same response shape—the SDK parses it with FilteredSchema or SortedSchema, guaranteeing type safety.

Practical Code Examples

Auto-Pagination with fetchMany

The SDK simplifies retrieving large datasets by handling pagination loops automatically:

import { Client } from "@common-grants/sdk";

const client = new Client({ 
  baseUrl: "https://api.commongrants.org", 
  pageSize: 50 
});

// Automatically fetches all pages until maxItems or end of data
const allOpportunities = await client.fetchMany<Opportunity>(
  "/common-grants/opportunities"
);

console.log(`Retrieved ${allOpportunities.items.length} total opportunities`);

Implementation details: fetchMany (lines 14‑49 in client.ts) repeatedly calls fetchOnePage (lines 60‑82), which attaches page and pageSize to query parameters for GET requests or to the request body for POST requests.

Search with Filters and Sorting

For complex queries, use the OpportunityClient to combine filtering and sorting:

import { OpportunityClient } from "@common-grants/sdk";

const oppClient = new OpportunityClient(client);

const result = await oppClient.searchOpportunities({
  query: "climate resilience",
  statuses: ["open", "pending"],  // Shorthand for status filter
  sorting: { 
    sortBy: "lastModifiedAt", 
    sortOrder: "desc" 
  },
  pagination: { page: 2, pageSize: 20 }  // Optional; auto-added if omitted
});

// Response includes metadata for all three concerns
console.log(result.paginationInfo.totalPages);
console.log(result.sortInfo.sortBy);
console.log(result.filterInfo.filters);

Implementation details: The buildSearchBody method (lines 74‑93 in opportunities.ts) converts shorthand parameters like statuses into formal filter objects. The fetchSearchPage method (lines 98‑110) merges pagination parameters and sends a POST request to /search, parsing the result with FilteredSchema.

Manual Pagination Control

For scenarios requiring explicit page navigation:

// Fetch specific page via GET
const page3 = await client.get("/common-grants/opportunities", {
  params: { 
    page: 3, 
    pageSize: 25,
    sortBy: "title",
    sortOrder: "asc"
  }
});

const data = await page3.json();
// data.paginationInfo contains totalItems, totalPages, isLastPage

Validation: Query parameters are validated against PaginatedQueryParamsSchema and SortQueryParamsSchema, ensuring page and pageSize are positive integers and sortOrder is either asc or desc.

Summary

CommonGrants delivers a consistent, type-safe protocol for list operations through these key mechanisms:

  • Zod schemas in pagination.ts, sorting.ts, and filters.ts enforce valid request parameters and response shapes across all endpoints.
  • Composable response wrappers (PaginatedSchema, SortedSchema, FilteredSchema) ensure every list response includes metadata about pagination state, sort criteria, and applied filters.
  • Auto-pagination via Client.fetchMany eliminates manual loop logic by automatically fetching pages until completion or a specified limit.
  • Unified request flow supports both GET (query parameters) and POST (JSON body) patterns while maintaining the same validation and response contracts.

Frequently Asked Questions

How does CommonGrants pagination work under the hood?

CommonGrants pagination relies on the PaginatedQueryParamsSchema and PaginatedBodyParamsSchema to validate that page and pageSize are positive integers. The server returns a PaginatedResultsInfoSchema object containing currentPage, pageSize, totalItems, and totalPages. In the TypeScript SDK, the fetchMany method in client.ts (lines 14‑49) automates the pagination loop by repeatedly calling fetchOnePage until the server indicates the last page via isLastPage or the client reaches a specified maxItems limit.

What filter operators are supported in CommonGrants?

The filters.ts file defines a comprehensive set of filter operators through enums like EquivalenceOperatorsEnum (equality, inequality) and ComparisonOperatorsEnum (greater than, less than, etc.). The protocol supports string comparisons, range filters, array inclusion checks, and custom implementation-defined operators. Each filter follows a strict schema such as DefaultFilterSchema or StringComparisonFilterSchema, ensuring type safety when constructing filter objects in POST request bodies or query parameters.

Can I combine pagination, filtering, and sorting in a single request?

Yes, CommonGrants is designed to compose all three concerns simultaneously. When using the OpportunityClient.searchOpportunities method, you can provide filters (such as statuses), sorting criteria (sortBy and sortOrder), and pagination parameters (page and pageSize). The SDK validates the combined request against the respective Zod schemas and returns a response parsed with FilteredSchema, which includes items, paginationInfo, sortInfo, and filterInfo, giving you complete visibility into how the results were generated.

How does the TypeScript SDK handle automatic pagination?

The SDK provides automatic pagination through the Client.fetchMany method, which abstracts the complexity of iterating through pages. When called, fetchMany invokes fetchOnePage to retrieve the first page of results, then continues requesting subsequent pages by incrementing the page parameter until the server returns isLastPage: true or the caller-specified maxItems threshold is reached. This mechanism works for both GET requests (where parameters are appended as query strings) and POST requests (where the pagination object is embedded in the JSON body), ensuring consistent behavior across different API endpoints.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →