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.
In this guide
- validated request
- server-owned context
- SDK-specific adapter
- declared endpoint
- paged response
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?
Independent guidance from GCG. Znode is a trademark of its owner. Examples use fictional data and are not official platform documentation. Suggest a correction.