Skip to content

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.

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[] } | AnyError

Each key in parameters is matched against the path:

  • Keys that appear as :name segments in the path become path parameters (URL-encoded and substituted in).
  • All other keys become query parameters. Optional parameters that are undefined or null are omitted from the URL.
  • Array values are joined with , by default; use serializationMethod to 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.

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:

FormatBehavior
'json' (default)JSON.stringify with content-type: application/json
'form-data'Encoded as FormDataBlob and string values appended directly, everything else JSON-stringified
'text'Sent as-is

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.' },
],
});

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 nextPageCursor
const pages = await fetchApiPages(listWidgets, { limit: 100 });
// Batching works on any top-level array parameter
const results = await fetchApiSplit(getWidgets, { ids: manyIds }, { ids: 50 });

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.