Skip to content
rest-rpc
Esc
navigateopen⌘Jpreview
On this page

Fetch Client

Use rest-rpc with Fetch

initClient() creates a typed fetch client from a contract.

import { initClient } from "@rest-rpc/core";
import { api } from "./contract";

const client = initClient(api, {
	baseUrl: "https://api.example.com",
});

The client has the same route tree shape as the contract.

const todo = await client.todos.get.fetch({ id: "todo_1" });

fetch()

Routes with one successful response expose fetch().

const todo = await client.todos.get.fetch({
	id: "todo_1",
});

fetch() returns the successful response body directly.

It throws when the response is undeclared or is not a declared success response.

Use it when non-success responses should use the error path.

fetchResponse()

Every HTTP route exposes fetchResponse().

const response = await client.todos.get.fetchResponse({
	id: "todo_1",
});

fetchResponse() returns declared and undeclared responses as values.

if (response.declared && response.status === 200) {
	return response.body;
}

if (response.declared && response.status === 404) {
	return undefined;
}

throw new Error(`Unexpected response: ${response.status}`);

Use it when HTTP status is part of normal control flow.

Client Options

const client = initClient(api, {
	baseUrl: "https://api.example.com",
	getGlobalHeaders: () => ({
		authorization: `Bearer ${readToken()}`,
	}),
	timeoutMs: 10_000,
	fetchOptions: {
		credentials: "include",
	},
});

Options include:

  • baseUrl
  • fetch
  • fetchOptions
  • getGlobalHeaders
  • timeoutMs
  • unknownRequestKeys
  • validateResponses

timeoutMs limits the time spent waiting for the configured fetch function to return a Response. It starts after getGlobalHeaders() resolves and is cleared as soon as the response is received. It does not include response body parsing, stream consumption, or other work before or after fetch. Pass an AbortSignal in a route call when you need to cancel later response or stream consumption.

Custom Fetch

If you need to wrap the fetch client with custom logic, pass a fetch function.

const client = initClient(api, {
	baseUrl: "https://api.example.com",
	fetch: async (url, init) => {
		const startedAt = performance.now();

		try {
			return await fetch(url, init);
		} finally {
			console.log("API request took", performance.now() - startedAt);
		}
	},
});

Per-Call Fetch Options

Pass fetch options as the second argument.

const todo = await client.todos.get.fetch(
	{
		id: "todo_1",
		"x-request-id": "req_1",
	},
	{
		cache: "no-store",
	},
);

Unknown Request Keys

By default, unknown request keys are rejected.

const client = initClient(api, {
	baseUrl: "https://api.example.com",
	unknownRequestKeys: "throw",
});

Use "strip" when extra keys should be ignored before building the HTTP request.

const client = initClient(api, {
	baseUrl: "https://api.example.com",
	unknownRequestKeys: "strip",
});

Response Validation on the Client

It is possible to enable server output validation on the client by enabling validateResponses. For understanding the implications of this option, read Schemas first.

const client = initClient(api, {
	baseUrl: "https://api.example.com",
	validateResponses: true,
});

This option also validates incoming WebSocket server messages on the client.

Use in Next.js

When using the core fetch client with Next.js, the client can automatically add deterministic tags to GET requests through Next’s fetch options.

import { initClient } from "@rest-rpc/core";
import { api } from "./contract";

export const client = initClient(api, {
	baseUrl: "https://api.example.com",
	nextFetchTags: {
		enabled: true,
		tagPrefix: "api",
	},
	fetchOptions: {
		next: {
			revalidate: 60,
			tags: ["your-custom-tag"],
		},
	},
});

For client.todos.get.fetch({ id: "todo_1" }), the client attaches both the exact request tag and the broader route tag.

["api:todos.get:id:todo_1", "api:todos.get"];

Use the route-level tag for broad invalidation. Use the exact request tag when the request identifies a stable cache variant.

Automatic tags only include path params and query params. Request bodies are not included because body-based requests generally should not be cached as GET fetches. Headers are also excluded because they commonly contain sensitive data, such as authorization or session values.

You can still pass Next.js fetch options normally through fetchOptions. Generated tags are merged with any tags you provide.

await client.todos.get.fetch(
	{ id: "todo_1" },
	{
		next: {
			revalidate: 60,
			tags: ["todos"],
		},
	},
);
"use server";

import { revalidateTag } from "next/cache";
import { getNextFetchTags } from "@rest-rpc/core";
import { api } from "./contract";
import { client } from "./client";

export async function renameTodo(id: string, title: string) {
	await client.todos.update.fetch({ id, title });

	for (const tag of getNextFetchTags(api.todos.get, { id }, { tagPrefix: "api" })) {
		revalidateTag(tag);
	}
}

WebSocket Routes

WebSocket routes expose openConnection() instead of fetch() and fetchResponse().

const socket = client.todos.watch.openConnection({
	id: "todo_1",
});

See WebSockets for details.

Was this page helpful?