Custom Endpoints
RoZod ships definitions for 810+ Roblox endpoints, but you can define your own — for undocumented Roblox APIs, your own backend, or any HTTP JSON API — and use them with fetchApi and all the other helpers with full type safety.
Basic Definition
Section titled “Basic Definition”import { z } from 'zod';import { endpoint, fetchApi } from 'rozod';
const getWidget = endpoint({ method: 'GET', path: '/v1/widgets/:widgetId', baseUrl: 'https://my-api.example.com', parameters: { widgetId: z.string(), verbose: z.boolean().optional(), }, response: z.object({ id: z.string(), name: z.string(), tags: z.array(z.string()), }),});
const widget = await fetchApi(getWidget, { widgetId: 'abc' });// widget is typed as { id: string; name: string; tags: string[] } | AnyErrorHow Parameters Are Mapped
Section titled “How Parameters Are Mapped”Each key in parameters is matched against the path:
- Keys that appear as
:namesegments in the path become path parameters (URL-encoded and substituted in). - All other keys become query parameters. Optional parameters that are
undefinedornullare omitted from the URL. - Array values are joined with
,by default; useserializationMethodto control this.
const searchWidgets = endpoint({ method: 'GET', path: '/v1/widgets', baseUrl: 'https://my-api.example.com', parameters: { ids: z.array(z.number()), limit: z.number().optional().default(10), }, serializationMethod: { ids: { style: 'form', explode: false }, // ?ids=1,2,3 // explode: true would produce ?ids=1&ids=2&ids=3 }, response: z.object({ data: z.array(z.unknown()) }),});Parameters declared with .default(...) are filled in automatically when you don’t pass them.
Request Bodies
Section titled “Request Bodies”For POST/PUT/PATCH endpoints, declare a body schema. It’s passed under the body key when calling:
const createWidget = endpoint({ method: 'POST', path: '/v1/widgets', baseUrl: 'https://my-api.example.com', body: z.object({ name: z.string(), tags: z.array(z.string()), }), response: z.object({ id: z.string() }),});
const created = await fetchApi(createWidget, { body: { name: 'My Widget', tags: ['new'] },});The requestFormat field controls body encoding:
| Format | Behavior |
|---|---|
'json' (default) | JSON.stringify with content-type: application/json |
'form-data' | Encoded as FormData — Blob and string values appended directly, everything else JSON-stringified |
'text' | Sent as-is |
Documenting Known Errors
Section titled “Documenting Known Errors”You can attach known error responses for reference (they don’t affect runtime behavior):
const getWidget = endpoint({ method: 'GET', path: '/v1/widgets/:widgetId', baseUrl: 'https://my-api.example.com', parameters: { widgetId: z.string() }, response: z.object({ id: z.string() }), errors: [ { status: 404, description: 'Widget not found.' }, ],});Works with Every Helper
Section titled “Works with Every Helper”Custom endpoints work with the whole toolkit — pagination, batching, caching, and long-running operations:
import { fetchApiPages, fetchApiSplit } from 'rozod';
// Pagination works if your response includes nextPageCursorconst pages = await fetchApiPages(listWidgets, { limit: 100 });
// Batching works on any top-level array parameterconst results = await fetchApiSplit(getWidgets, { ids: manyIds }, { ids: 50 });Non-Roblox Domains
Section titled “Non-Roblox Domains”Custom endpoints aren’t limited to Roblox. For non-Roblox domains, error responses are parsed with a best-effort fallback that looks for common fields (message, error, error_message, detail) instead of the Roblox error format.