Relay

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.

JavaScript
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.

JavaScript
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.

JavaScript
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.

OptionDefaultDescription
limit2Maximum 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 ** nDelay in milliseconds before attempt n, with jitter.
respectRetryAftertrueWait 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.

JavaScript
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:

CodeWhen it happens
HTTP_ERRORThe server responded with a 4xx or 5xx status. error.response is available.
TIMEOUTAn attempt took longer than timeout.
NETWORK_ERRORThe request couldn't reach the server (offline, DNS or CORS failure).
ABORTEDYour AbortSignal cancelled the request.
PARSE_ERRORA 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:

status.test.js
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?

Edit this page on GitHub

Last updated

© 2026 Relay contributors. Released under the MIT License.

v3.2.0 · FAQ · GitHub