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

> Learn how CommonGrants uses Zod schemas for type-safe pagination filtering and sorting. Discover HHS simpler grants protocol's automatic client-side SDK handling.

- Repository: [U.S. Department of Health & Human Services/simpler-grants-protocol](https://github.com/hhs/simpler-grants-protocol)
- Tags: technical-guide
- Published: 2026-03-03

---

**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`](https://github.com/hhs/simpler-grants-protocol/blob/main/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`](https://github.com/hhs/simpler-grants-protocol/blob/main/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`](https://github.com/hhs/simpler-grants-protocol/blob/main/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`](https://github.com/hhs/simpler-grants-protocol/blob/main/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:

```typescript
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`](https://github.com/hhs/simpler-grants-protocol/blob/main/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`](https://github.com/hhs/simpler-grants-protocol/blob/main/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`](https://github.com/hhs/simpler-grants-protocol/blob/main/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`](https://github.com/hhs/simpler-grants-protocol/blob/main/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:

```typescript
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`](https://github.com/hhs/simpler-grants-protocol/blob/main/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:

```typescript
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`](https://github.com/hhs/simpler-grants-protocol/blob/main/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:

```typescript
// 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`](https://github.com/hhs/simpler-grants-protocol/blob/main/pagination.ts), [`sorting.ts`](https://github.com/hhs/simpler-grants-protocol/blob/main/sorting.ts), and [`filters.ts`](https://github.com/hhs/simpler-grants-protocol/blob/main/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`](https://github.com/hhs/simpler-grants-protocol/blob/main/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`](https://github.com/hhs/simpler-grants-protocol/blob/main/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.