Core concepts
How a request moves through Relay, from options and timeouts to retries, interceptors and errors. Read this once and the rest of the API will feel predictable.
On this page
#Requests and responses
Every call returns a RelayRequest: a promise for the Response that also has body helpers. Calling a helper sets the matching Accept header and parses the body for you.
const res = await relay.get('reports/2026.csv'); // a Response
const text = await relay.get('reports/2026.csv').text();
const data = await relay.get('reports/latest').json();#Request options
Options are plain objects. Anything Relay doesn't recognise is passed straight to fetch, so credentials, cache and signal all work as usual.
#Sending data
Use json for JSON bodies and body for anything else, such as FormData or a Blob. Relay sets Content-Type for you when it can.
await relay.post('contacts', { json: { name: 'Ada', role: 'admin' } });
const form = new FormData();
form.append('avatar', file);
await relay.put('me/avatar', { body: form });#Query strings
searchParams accepts an object, a string or a URLSearchParams. Undefined values are skipped, which keeps optional filters tidy.
#Timeouts and cancellation
Requests time out after 10 seconds by default. Set timeout per request or on an instance, or pass false to disable it.
const controller = new AbortController();
const request = relay.get('search', { searchParams: { q }, signal: controller.signal, timeout: 3000 });
// Cancel when the user types again
input.addEventListener('input', () => controller.abort(), { once: true });#Retries
Relay retries failed requests that are safe to repeat. By default that's GET, HEAD, PUT, DELETE and OPTIONS requests that fail with a network error or one of these statuses: 408, 413, 429, 500, 502, 503 or 504.
| Option | Default | Description |
|---|---|---|
limit | 2 | Maximum number of retries after the first attempt. |
methods | ['get','put','head','delete','options'] | Methods that are retried. |
statusCodes | [408,413,429,500,502,503,504] | Response statuses that trigger a retry. |
backoff | (n) => 300 * 2 ** n | Delay in milliseconds before attempt n, with jitter. |
respectRetryAfter | true | Wait for the server's Retry-After header when present. |
#Interceptors
Interceptors run code before a request is sent or after a response arrives. They're how you add auth headers, log timings or refresh expired tokens in one place.
api.interceptors.request.use((request) => {
request.headers.set('authorization', `Bearer ${session.token}`);
});
api.interceptors.response.use(async (response, request) => {
if (response.status === 401 && !request.retried) {
await session.refresh();
return api.retry(request);
}
return response;
});Interceptors run in the order they were added. Each use() call returns a function that removes the interceptor again.
#Error handling
All failures reject with a RelayError. Check error.code to tell them apart:
| Code | When it happens |
|---|---|
HTTP_ERROR | The server responded with a 4xx or 5xx status. error.response is available. |
TIMEOUT | An attempt took longer than timeout. |
NETWORK_ERROR | The request couldn't reach the server (offline, DNS or CORS failure). |
ABORTED | Your AbortSignal cancelled the request. |
PARSE_ERROR | A body helper such as .json() couldn't parse the response. |
#Testing
Pass your own fetch to an instance to test code that uses Relay without touching the network:
import { test, expect } from 'vitest';
const fakeFetch = async () => new Response(JSON.stringify({ ok: true }), { status: 200 });
const api = relay.create({ fetch: fakeFetch });
test('reads the status', async () => {
expect(await api.get('status').json()).toEqual({ ok: true });
});Was this page helpful?
Last updated