Validate a paged API request with bounded filters and server-owned context

Parse URL or JSON paging input into a small typed request, enforce limits and allowed filters, and resolve account authorization separately.

Znode 10RecipeFoundationGCG engineering guide
In this guide
  1. validated request
  2. server-owned context
  3. SDK-specific adapter
  4. declared endpoint
  5. paged response
Validation restricts the query shape while the server supplies the context that determines accessible records.

Define a narrow request shape

Use a small request model for one paged resource: page, pageSize, and optional status. Keep Store, account, and permission context out of this model. Resolve them from trusted server authentication before reading protected data.

normalizePage accepts unknown input because TypeScript types do not validate incoming JSON. It rejects extra fields, arrays, invalid status values, and paging values outside this example's policy. It returns a new PageRequest containing only the allowed fields.

pagedRead.ts typescript

type PageRequest = {
  page: number;
  pageSize: number;
  status?: 'active' | 'archived';
};

export function normalizePage(input: unknown): PageRequest {
  if (typeof input !== 'object' || input === null || Array.isArray(input)) {
    throw new Error('A request object is required');
  }
  const value = input as Record<string, unknown>;
  const allowed = new Set(['page', 'pageSize', 'status']);
  if (Object.keys(value).some(key => !allowed.has(key))) {
    throw new Error('Unsupported request field');
  }
  const { page, pageSize, status } = value;
  if (typeof page !== 'number' || !Number.isSafeInteger(page) || page < 1 || page > 1000) {
    throw new Error('page must be 1 through 1000');
  }
  if (typeof pageSize !== 'number' || !Number.isSafeInteger(pageSize) || pageSize < 1 || pageSize > 50) {
    throw new Error('pageSize must be 1 through 50');
  }
  if (status !== undefined && status !== 'active' && status !== 'archived') {
    throw new Error('Unsupported status filter');
  }
  return { page, pageSize, ...(status === undefined ? {} : { status }) };
}

// These caps are fictional application policy, not Znode limits.
// A verified SDK adapter maps this object to one declared endpoint.

The object { page: 2, pageSize: 25, status: 'active' } is accepted. A string page value such as '2' is rejected, as is an accountId field. For an HTTP URL such as '?page=2&pageSize=25', use the supplemental pageQuery adapter to parse strings first, then apply this same validation.

Keep filters typed and finite

Map PageRequest into the installed endpoint's paging and filter contract on the server. Translate only active and archived into the approved status filter. Use the SDK or HTTP library's encoding; do not pass a client-supplied filter expression through to the API.

Define a stable sort with a unique tie-breaker for paging. Return an empty list and paging metadata for a permitted read with no matches. An unauthorized read is a different outcome and must not be disguised as a successful empty page.

Verify page boundaries

Validate null, arrays, numeric strings, extra fields, unsupported status, unsafe integers, and values just outside the page and size limits. These cases check request parsing before an API call happens.

Then exercise the adapter with synthetic records spanning several pages. Verify ordering, no duplicates or omissions, a short final page, no matches, and a denied account context. The parser validates request shape; the endpoint adapter must prove the actual read, authorization, and paging behavior.

Parse URL query values before applying the page policy

Place this beside pagedRead.ts and call pageQuery with URL.searchParams in the server adapter. '?page=2&pageSize=25&status=active' becomes numeric page 2 and pageSize 25 before normalizePage checks the bounds. Missing page and pageSize default to 1 and 25. Duplicate parameters, unsupported fields, and noncanonical numbers such as 02 are rejected. Resolve Store, account, and authorization separately.

page-query.ts typescript

import { normalizePage } from "./pagedRead";

export function pageQuery(params: URLSearchParams): ReturnType<typeof normalizePage> {
  const allowed = new Set(["page", "pageSize", "status"]);
  for (const key of params.keys()) {
    if (!allowed.has(key) || params.getAll(key).length !== 1)
      throw new Error("Unsupported or duplicate query parameter.");
  }
  const positive = (name: string, fallback: number): number => {
    const raw = params.get(name);
    if (raw === null) return fallback;
    if (!/^[1-9][0-9]{0,8}$/.test(raw))
      throw new Error("Use a positive decimal page value.");
    return Number(raw);
  };
  const status = params.get("status");
  return normalizePage({
    page: positive("page", 1),
    pageSize: positive("pageSize", 25),
    ...(status === null ? {} : { status })
  });
}

References and further reading

Bring your next engineering question.

Need an integration read path scoped and tested?

Explore how GCG can help

Independent guidance from GCG. Znode is a trademark of its owner. Examples use fictional data and are not official platform documentation. Suggest a correction.

Previous
Previous

Render a product attribute safely in a Znode 9 Razor view

Next
Next

Native account price lists: verify the association and the storefront