Concepts
How pagination, errors, hooks, retries, abort signals, and custom fetch work - with runnable demonstrations of each.
Pagination
Every list() method returns a
Page<T> - a frozen snapshot of the items on the
current page plus navigation helpers. Pagination is offset-based: page
size must be one of 6, 12, 24, 48, 100 (the server
rejects everything else).
// Get page 1, then call .next() to fetch page 2.
let page = await client.prompts.list({ scope: "public", pageSize: 6 });
console.log(`page ${page.page}: ${page.items.length} items, hasNext=${page.hasNext}`);
if (page.hasNext) {
page = await page.next();
console.log(`page ${page.page}: ${page.items.length} items, hasPrev=${page.hasPrev}`);
}
return {
currentPage: page.page,
pageSize: page.pageSize,
total: page.total,
hasNext: page.hasNext,
hasPrev: page.hasPrev,
};
Calling .next() on the last page (or
.prev() on the first) rejects with a
RangeError. Always check hasNext /
hasPrev before navigating.
Async iteration with listAll()
For batch jobs, listAll() is an async iterator that walks
every page and yields items one at a time. Pagination is handled for
you.
const titles = [];
let count = 0;
for await (const prompt of client.prompts.listAll({ scope: "public", pageSize: 12 })) {
titles.push(prompt.title);
if (++count >= 24) break;
}
console.log(`Collected ${titles.length} titles across pages.`);
return titles;
Error handling
Every non-2xx response throws a typed subclass of
PromptyError. Use instanceof to narrow:
PromptyError // base class
├── PromptyConfigError // bad config (sync, at construction)
├── PromptyNetworkError // fetch rejected (DNS, TLS, offline)
├── PromptyTimeoutError // request exceeded timeoutMs
└── PromptyHttpError // base for non-2xx responses
├── PromptyValidationError // 400
├── PromptyAuthError // 401
├── PromptyNotFoundError // 404
├── PromptyRateLimitError // 429 (carries optional retryAfter)
└── PromptyServerError // 5xx
try {
await client.prompts.get("definitely-not-an-id");
} catch (err) {
if (err instanceof PromptyNotFoundError) {
console.log("got 404:", err.message, "status=", err.status);
return { caught: "PromptyNotFoundError", message: err.message };
}
if (err instanceof PromptyAuthError) {
return { caught: "PromptyAuthError", message: err.message };
}
if (err instanceof PromptyError) {
return { caught: err.name, message: err.message };
}
throw err;
}
All errors carry status, optionally
requestId (from the x-request-id response
header), and the raw response. PromptyRateLimitError
additionally exposes retryAfter when the server provided a
Retry-After header.
Request & response hooks
onRequest fires before every fetch;
onResponse fires after every fetch (success or error)
with timing data. Both can be sync or async - async hooks are awaited
before the request continues. Useful for tracing, logging, metrics.
// Build a fresh client with hooks attached, reusing the same API key.
const traced = [];
const observed = createPromptyClient({
apiKey: localStorage.getItem("prompty-tools-core/api-key") ?? "",
onRequest: (ctx) =>
traced.push({ phase: "request", method: ctx.method, url: ctx.url }),
onResponse: (ctx) =>
traced.push({
phase: "response",
status: ctx.status,
ok: ctx.ok,
durationMs: ctx.durationMs,
}),
});
await observed.prompts.list({ pageSize: 6 });
return traced;
Retries
Retries are off by default. When you opt in via
maxRetries, only GET requests are retried,
and only on 429 or 5xx responses (plus
timeouts/network errors are not retried - they're surfaced
immediately). Backoff is exponential with jitter, capped at 30
seconds.
Mutations are never retried automatically - there's no idempotency-key protocol on the server, so the client refuses to risk duplicate writes. Wrap mutations in your own retry logic if you need it.
import { createPromptyClient } from "@prompty-tools/core";
const client = createPromptyClient({
apiKey: process.env.PROMPTY_API_KEY,
maxRetries: 3, // GET-only, on 429/5xx
});
Aborting requests
Every list method accepts an AbortSignal via
params.signal. The detail/mutation methods don't take an
explicit signal - for those, configure a client-level
timeoutMs instead, or pass a custom
fetch that wraps an AbortController.
const controller = new AbortController();
// Abort almost immediately - well before the network round-trip completes.
setTimeout(() => controller.abort(), 1);
try {
await client.prompts.list({ scope: "public", signal: controller.signal });
return { aborted: false };
} catch (err) {
return { aborted: true, error: `${err.name}: ${err.message}` };
}
Bringing your own fetch
Every option is wired through a single fetch seam - pass
your own implementation to add tracing, run on edge runtimes with
custom request handling, or stub the network in tests:
import { createPromptyClient } from "@prompty-tools/core";
const client = createPromptyClient({
apiKey: process.env.PROMPTY_API_KEY!,
fetch: async (input, init) => {
const start = performance.now();
const res = await fetch(input, init);
const ms = performance.now() - start;
metrics.histogram("prompty.latency", ms, { url: String(input) });
return res;
},
});
The default is globalThis.fetch. The same seam is what
powers the package's own test suite - there's no MSW or
nock involved, just an injected fake fetch.
let captured;
const wrapped = createPromptyClient({
apiKey: localStorage.getItem("prompty-tools-core/api-key") ?? "",
fetch: async (input, init) => {
const merged = {
...init,
headers: { ...init?.headers, "x-prompty-trace": "demo" },
};
captured = { url: String(input), headers: merged.headers };
return fetch(input, merged);
},
});
await wrapped.prompts.list({ pageSize: 6 });
return captured;