Skip to content
rest-rpc
Esc
↑↓navigate↵open⌘Jpreview
On this page

Fetch Client

Create a typed Fetch client to call your routes over HTTP.

initClient() creates a typed Fetch client from a runtime contract. When the routes are implemented in the server, you need to Generate a Contract first.

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

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

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

Call Routes

The route declaration determines the request and result shapes. Flat input stays flat, and segmented input uses params, query, headers, and body.

const todo = await client.todos.create({ title: "Write docs" });
const response = await client.todos.get({ params: { id: todo.id } });

Plain output routes return decoded data directly. Status response routes return a status-discriminated union with body and the Fetch Headers object as headers. Declared response headers are also exposed through responseHeaders.

if (response.status === 200) {
	console.log(response.body.title);
} else if (response.status === 404) {
	console.log(response.body.code);
}

Declared non-2xx responses are returned as envelopes. Undeclared statuses throw an HttpError. Network failures and aborted requests reject with the error from fetch. See the Route Builder for choosing result forms and declaring expected outcomes.

Error Handling

An HttpError is thrown when a response does not match the route contract:

  • The status is not declared by the route. A plain output route only accepts its successful status.
  • The response content type is not one the route declares.
  • Response validation is enabled and fails. This can happen with a 2xx status.

Its status is the received HTTP status, and its body is the decoded, unvalidated response body. Validation issues are available through cause.

import { HttpError } from "@rest-rpc/core";

try {
	await client.todos.get({ params: { id: "todo_1" } });
} catch (error) {
	if (error instanceof HttpError) {
		console.log(error.status, error.body);
	} else {
		throw error;
	}
}

Consume Streams

A streaming route returns an async iterable of SseEvent<T> values. Application data is available on event.data:

export type SseEvent<T> = {
	data: T;
	id?: string;
	event?: string;
	retry?: number;
};
for await (const event of await client.todos.events()) {
	console.log(event.data.message);
	console.log(event.id, event.event, event.retry);
}

For a status response stream, narrow the response before iterating its body:

const response = await client.todos.events();

if (response.status === 200) {
	for await (const event of response.body) {
		console.log(event.data.message);
	}
}

The initial request and later stream iteration can fail independently. Pass an AbortSignal in the per-call options to cancel both the request and stream:

const controller = new AbortController();
const events = await client.todos.events(undefined, {
	signal: controller.signal,
});

controller.abort();

Reconnecting

The client reads SSE events over Fetch but is not a replacement for the browser EventSource API. The id, event, and retry fields are exposed as metadata only: the client does not reconnect automatically, send the last event ID, or filter events by name.

To resume a stream, send the last received event ID in the last-event-id header. The server receives it as lastEventId.

let lastEventId: string | undefined;

async function connect() {
	const events = await client.todos.events(undefined, {
		additionalHeaders: {
			"last-event-id": lastEventId,
		},
	});

	for await (const event of events) {
		lastEventId = event.id ?? lastEventId;
		handleTodoEvent(event.data);
	}
}

Client Options

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

Base URL used to build route requests.

Typestring
fetch?FetchLike

Custom fetch-compatible function used to send requests.

TypeFetchLike
fetchOptions?ApiClientFetchOptions

Default fetch options merged into every request. Excludes `signal`, which can only be passed per call.

TypeApiClientFetchOptions
bodyCodecs?readonly BodyCodec<Response>[]

Custom body codecs that take precedence over the defaults. See Serialization and Codecs.

Typereadonly BodyCodec<Response>[]
globalHeaders?ClientHeaders

Headers added to every request. Each value may be static or provided for each request.

TypeClientHeaders
timeoutMs?number

Aborts a request when fetch does not return in time.

Typenumber
validateResponses?boolean

Validates response bodies, declared response headers, and stream event data against the contract's schemas.

Typeboolean
Defaultfalse

See Serialization and Codecs for the default body codecs and how to override them with bodyCodecs.

Global Headers

globalHeaders adds headers to every request. Values can be static or provided by synchronous or asynchronous functions. A provider is useful for values that can change during the lifetime of the client, such as an access token.

const client = initClient(api, {
	baseUrl: "https://api.example.com",
	globalHeaders: {
		authorization: () => `Bearer ${readToken()}`,
		"x-client-version": "2026-09",
	},
});

A global header with a guaranteed value makes a matching declared route header optional in client calls. A provider that can return undefined does not.

Routes that combine .input() with .headers() only accept the input in client calls, so their declared headers must be supplied through globalHeaders.

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 Options

Pass fetch options as the second argument. They override matching defaults in fetchOptions. Use signal for cancellation. timeoutMs still applies when a signal is passed.

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

Use additionalHeaders for transport headers needed only by this call. Headers declared by the route remain part of the typed request object.

const response = await client.todos.get(
	{ params: { id: "todo_1" } },
	{
		additionalHeaders: {
			"x-trace-id": traceId,
		},
	},
);

When a route declares a list of request content types, contentType selects the one used to encode the body and is required:

await client.files.upload(bytes, { contentType: "image/png" });

If you need to pass options to a route without request input, pass undefined for the request input.

const response = await client.health(undefined, {
	cache: "no-store",
});

Response Validation

The client decodes response bodies without schema validation by default. Enable validateResponses to validate decoded bodies, declared response headers, and stream event data:

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

The client receives the schema output on success, and a failure throws an HttpError. The decoded response is the server’s schema output, but the client validates it as schema input. Schemas whose input and output types differ, such as transforms, may fail on the client.

Type Helpers

Use the type helpers when a client request or result type needs to be reused, for example in your own wrapper functions.

import type {
	
type InferClientError<T extends RouteTree> = T extends {
    readonly "~restrpc": infer TRoute extends RouteDeclaration;
} ? ClientErrorForDeclaration<TRoute> : { [K in keyof T]: T[K] extends RouteTree ? InferClientError<T[K]> : never; }
Infers the declared error responses a route's client call can return.
@remarksIncludes the envelopes of declared non-2xx responses. Routes declared with `.output()` infer `never` because the client throws their non-2xx responses. Thrown `HttpError` and `Error` values are not included. Pass a route tree to infer a matching tree of types.@see{@link https://rest-rpc.dev/docs/client/fetch-client#type-helpers}
InferClientError
,
type InferClientRequest<T extends RouteTree, TGlobalHeaders extends ClientHeaders = Record<never, string>> = T extends {
    readonly "~restrpc": infer TRoute extends RouteDeclaration;
} ? ClientRequestValue<ClientRequestForDeclaration<TRoute, GlobalHeaderKeys<TGlobalHeaders>>> : { [K in keyof T]: T[K] extends RouteTree ? InferClientRequest<T[K], TGlobalHeaders> : never; }
Infers the input passed to a generated client route call.
@remarksRoutes without request input infer `undefined`, which the route call accepts. Pass the type of the client's `globalHeaders` as the second argument to make headers that it always provides optional. Pass a route tree to infer a matching tree of request types.@see{@link https://rest-rpc.dev/docs/client/fetch-client#type-helpers}
InferClientRequest
,
type InferClientResponse<T extends RouteTree> = T extends {
    readonly "~restrpc": infer TRoute extends RouteDeclaration;
} ? ClientResponseForDeclaration<TRoute> : { [K in keyof T]: T[K] extends RouteTree ? InferClientResponse<T[K]> : never; }
Infers a route's client result.
@remarksRoutes declared with `.response()` produce status-discriminated envelopes. Routes declared with `.output()` produce their output directly. Pass a route tree to infer a matching tree of results.@see{@link https://rest-rpc.dev/docs/client/fetch-client#type-helpers}@see{@link https://rest-rpc.dev/docs/client/fetch-client#call-routes}
InferClientResponse
,
type InferClientStreamData<T extends RouteTree> = T extends {
    readonly "~restrpc": infer TRoute extends RouteDeclaration;
} ? ClientStreamDataForDeclaration<TRoute> : { [K in keyof T]: T[K] extends RouteTree ? InferClientStreamData<T[K]> : never; }
Infers the `data` of each event received from a streaming route.
@remarksUnwraps the `SseEvent` values of successful stream responses. Routes without a stream response infer `never`. Pass a route tree to infer a matching tree of event data types.@see{@link https://rest-rpc.dev/docs/client/fetch-client#type-helpers}@see{@link https://rest-rpc.dev/docs/client/fetch-client#consume-streams}
InferClientStreamData
,
type InferClientSuccess<T extends RouteTree> = T extends {
    readonly "~restrpc": infer TRoute extends RouteDeclaration;
} ? ClientSuccessForDeclaration<TRoute> : { [K in keyof T]: T[K] extends RouteTree ? InferClientSuccess<T[K]> : never; }
Infers the successful result of a route's client call.
@remarksRoutes declared with `.response()` produce the envelopes of their 2xx responses. Routes declared with `.output()` produce their output directly. Pass a route tree to infer a matching tree of results.@see{@link https://rest-rpc.dev/docs/client/fetch-client#type-helpers}
InferClientSuccess
,
} from "@rest-rpc/core"; import { const route: ServerRouteBuilder<ExpressHandlerFields, DefaultContext>
Entry point for declaring routes with handlers for Express.
@remarksResponses are inferred from the handler unless they are declared before `.handler()`.@see{@link https://rest-rpc.dev/docs/quickstart#define-and-register-routes}@see{@link https://rest-rpc.dev/docs/server/express#framework-context}
route
} from "@rest-rpc/express";
import import zz from "zod"; const
const update: HandlerImplementation<{
    readonly kind: "http";
    readonly method: "PATCH";
    readonly path: "/todos/:id";
    request: {
        body: readonly [z.ZodObject<{
            title: z.ZodString;
        }, z.core.$strip>];
        params: readonly [z.ZodObject<{
            id: z.ZodString;
        }, z.core.$strip>];
        contentType: "application/json";
    };
    responses: {};
    readonly input: "segments";
}, ExpressHandlerFields, DefaultContext, {
    readonly id: string;
    readonly title: string;
}, {
    readonly input: "segments";
    readonly method: "PATCH";
    readonly path: "/todos/:id";
    request: {
        body: readonly [z.ZodObject<{
            title: z.ZodString;
        }, z.core.$strip>];
        params: readonly [z.ZodObject<{
            id: z.ZodString;
        }, z.core.$strip>];
        contentType: "application/json";
    };
    readonly kind: "http";
    responses: {
        ...;
    };
    output: "output";
}>
update
= const route: ServerRouteBuilder<ExpressHandlerFields, DefaultContext>
Entry point for declaring routes with handlers for Express.
@remarksResponses are inferred from the handler unless they are declared before `.handler()`.@see{@link https://rest-rpc.dev/docs/quickstart#define-and-register-routes}@see{@link https://rest-rpc.dev/docs/server/express#framework-context}
route
.patch<"/todos/:id", never>(this: BuilderReceiver<BuilderPath, never>, path?: "/todos/:id" | undefined): RouteBuilderView<WithHttpRoute<DerivedState, "PATCH">, ServerBuilderExtension<ExpressHandlerFields, DefaultContext>, "/todos/:id", never>patch("/todos/:id") .
params<z.ZodObject<{
    id: z.ZodString;
}, z.core.$strip>, "/todos/:id", never>(this: BuilderReceiver<"/todos/:id", never>, schema: z.ZodObject<{
    id: z.ZodString;
}, z.core.$strip>): RouteBuilderView<WithRequest<WithHttpRoute<DerivedState, "PATCH">, "params", z.ZodObject<{
    id: z.ZodString;
}, z.core.$strip>>, ServerBuilderExtension<ExpressHandlerFields, DefaultContext>, "/todos/:id", never>
Declares path parameters.
@see{@link https://rest-rpc.dev/docs/route-builder#declare-request-segments}
params
(import zz.
function object<{
    id: z.ZodString;
}>(shape?: {
    id: z.ZodString;
} | undefined, params?: string | {
    error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueInvalidType<unknown> | z.core.$ZodIssueUnrecognizedKeys>> | undefined;
    message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
    id: z.ZodString;
}, z.core.$strip>
object
({ id: z.ZodStringid: import zz.function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)string() }))
.
body<z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>, undefined, "/todos/:id", never>(this: BuilderReceiver<"/todos/:id", never>, schema: z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>, options?: undefined): RouteBuilderView<WithContentType<WithRequest<WithRequest<WithHttpRoute<DerivedState, "PATCH">, "params", z.ZodObject<{
    id: z.ZodString;
}, z.core.$strip>>, "body", z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>>, "application/json">, ServerBuilderExtension<...>, "/todos/:id", never>
Declares a request body and its HTTP metadata.
@see{@link https://rest-rpc.dev/docs/route-builder#declare-request-segments}
body
(import zz.
function object<{
    title: z.ZodString;
}>(shape?: {
    title: z.ZodString;
} | undefined, params?: string | {
    error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueInvalidType<unknown> | z.core.$ZodIssueUnrecognizedKeys>> | undefined;
    message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>
object
({ title: z.ZodStringtitle: import zz.function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)string() }))
.
handler<{
    readonly id: string;
    readonly title: string;
}>(handler: HandlerFor<{
    readonly kind: "http";
    readonly method: "PATCH";
    readonly path: "/todos/:id";
    request: {
        body: readonly [z.ZodObject<{
            title: z.ZodString;
        }, z.core.$strip>];
        params: readonly [z.ZodObject<{
            id: z.ZodString;
        }, z.core.$strip>];
        contentType: "application/json";
    };
    responses: {};
    readonly input: "segments";
}, ExpressHandlerFields, DefaultContext, {
    readonly id: string;
    readonly title: string;
}>): HandlerImplementation<{
    readonly kind: "http";
    readonly method: "PATCH";
    readonly path: "/todos/:id";
    request: {
        body: readonly [z.ZodObject<{
            title: z.ZodString;
        }, z.core.$strip>];
        params: readonly [z.ZodObject<{
            id: z.ZodString;
        }, z.core.$strip>];
        contentType: "application/json";
    };
    responses: {};
    readonly input: "segments";
}, ExpressHandlerFields, DefaultContext, {
    readonly id: string;
    readonly title: string;
}, {
    ...;
}>
handler
(({
params: {
    id: string;
}
params
,
body: {
    title: string;
}
body
}) => ({ id: stringid:
params: {
    id: string;
}
params
.id: stringid, title: stringtitle:
body: {
    title: string;
}
body
.title: stringtitle }));
const
const getFirst: HandlerImplementation<{
    readonly kind: "procedure";
    readonly method: "POST";
    readonly path: undefined;
    request?: never | undefined;
    responses: {};
}, ExpressHandlerFields, DefaultContext, {
    readonly status: 200;
    readonly body: {
        readonly id: "todo_1";
        readonly title: "Write docs";
        readonly code?: undefined;
    };
} | {
    readonly status: 404;
    readonly body: {
        readonly code: "TODO_NOT_FOUND";
        readonly id?: undefined;
        readonly title?: undefined;
    };
}, {
    readonly method: "POST";
    readonly path: undefined;
    request?: never | undefined;
    readonly kind: "procedure";
    responses: InferredResponses<...>;
    output: "response";
}>
getFirst
= const route: ServerRouteBuilder<ExpressHandlerFields, DefaultContext>
Entry point for declaring routes with handlers for Express.
@remarksResponses are inferred from the handler unless they are declared before `.handler()`.@see{@link https://rest-rpc.dev/docs/quickstart#define-and-register-routes}@see{@link https://rest-rpc.dev/docs/server/express#framework-context}
route
.
handler<{
    readonly status: 200;
    readonly body: {
        readonly id: "todo_1";
        readonly title: "Write docs";
        readonly code?: undefined;
    };
} | {
    readonly status: 404;
    readonly body: {
        readonly code: "TODO_NOT_FOUND";
        readonly id?: undefined;
        readonly title?: undefined;
    };
}>(handler: HandlerFor<{
    readonly kind: "procedure";
    readonly method: "POST";
    readonly path: undefined;
    request?: never | undefined;
    responses: {};
}, ExpressHandlerFields, DefaultContext, {
    readonly status: 200;
    readonly body: {
        readonly id: "todo_1";
        readonly title: "Write docs";
        readonly code?: undefined;
    };
} | {
    readonly status: 404;
    readonly body: {
        readonly code: "TODO_NOT_FOUND";
        readonly id?: undefined;
        readonly title?: undefined;
    };
}>): HandlerImplementation<...>
handler
(() => {
if (var Math: Math
An intrinsic object that provides basic mathematics functionality and constants.
Math
.Math.random(): number
Returns a pseudorandom number between 0 and 1.
random
() < 0.5) {
return { status: 200status: 200,
body: {
    readonly id: "todo_1";
    readonly title: "Write docs";
    readonly code?: undefined;
}
body
: { id: "todo_1"id: "todo_1", title: "Write docs"title: "Write docs" } };
} return { status: 404status: 404,
body: {
    readonly code: "TODO_NOT_FOUND";
    readonly id?: undefined;
    readonly title?: undefined;
}
body
: { code: "TODO_NOT_FOUND"code: "TODO_NOT_FOUND" as type const = "TODO_NOT_FOUND"const } };
}); type
type UpdateTodoRequest = {
    body: {
        title: string;
    };
    params: {
        id: string;
    };
}
UpdateTodoRequest
=
type InferClientRequest<T extends RouteTree, TGlobalHeaders extends ClientHeaders = Record<never, string>> = T extends {
    readonly "~restrpc": infer TRoute extends RouteDeclaration;
} ? ClientRequestValue<ClientRequestForDeclaration<TRoute, GlobalHeaderKeys<TGlobalHeaders>>> : { [K in keyof T]: T[K] extends RouteTree ? InferClientRequest<T[K], TGlobalHeaders> : never; }
Infers the input passed to a generated client route call.
@remarksRoutes without request input infer `undefined`, which the route call accepts. Pass the type of the client's `globalHeaders` as the second argument to make headers that it always provides optional. Pass a route tree to infer a matching tree of request types.@see{@link https://rest-rpc.dev/docs/client/fetch-client#type-helpers}
InferClientRequest
<typeof
const update: HandlerImplementation<{
    readonly kind: "http";
    readonly method: "PATCH";
    readonly path: "/todos/:id";
    request: {
        body: readonly [z.ZodObject<{
            title: z.ZodString;
        }, z.core.$strip>];
        params: readonly [z.ZodObject<{
            id: z.ZodString;
        }, z.core.$strip>];
        contentType: "application/json";
    };
    responses: {};
    readonly input: "segments";
}, ExpressHandlerFields, DefaultContext, {
    readonly id: string;
    readonly title: string;
}, {
    readonly input: "segments";
    readonly method: "PATCH";
    readonly path: "/todos/:id";
    request: {
        body: readonly [z.ZodObject<{
            title: z.ZodString;
        }, z.core.$strip>];
        params: readonly [z.ZodObject<{
            id: z.ZodString;
        }, z.core.$strip>];
        contentType: "application/json";
    };
    readonly kind: "http";
    responses: {
        ...;
    };
    output: "output";
}>
update
>;
type
type GetFirstTodoResponse = {
    status: 200;
    body: {
        readonly id: "todo_1";
        readonly title: "Write docs";
        readonly code?: undefined;
    };
    headers: Headers;
} | {
    status: 404;
    body: {
        readonly code: "TODO_NOT_FOUND";
        readonly id?: undefined;
        readonly title?: undefined;
    };
    headers: Headers;
}
GetFirstTodoResponse
=
type InferClientResponse<T extends RouteTree> = T extends {
    readonly "~restrpc": infer TRoute extends RouteDeclaration;
} ? ClientResponseForDeclaration<TRoute> : { [K in keyof T]: T[K] extends RouteTree ? InferClientResponse<T[K]> : never; }
Infers a route's client result.
@remarksRoutes declared with `.response()` produce status-discriminated envelopes. Routes declared with `.output()` produce their output directly. Pass a route tree to infer a matching tree of results.@see{@link https://rest-rpc.dev/docs/client/fetch-client#type-helpers}@see{@link https://rest-rpc.dev/docs/client/fetch-client#call-routes}
InferClientResponse
<typeof
const getFirst: HandlerImplementation<{
    readonly kind: "procedure";
    readonly method: "POST";
    readonly path: undefined;
    request?: never | undefined;
    responses: {};
}, ExpressHandlerFields, DefaultContext, {
    readonly status: 200;
    readonly body: {
        readonly id: "todo_1";
        readonly title: "Write docs";
        readonly code?: undefined;
    };
} | {
    readonly status: 404;
    readonly body: {
        readonly code: "TODO_NOT_FOUND";
        readonly id?: undefined;
        readonly title?: undefined;
    };
}, {
    readonly method: "POST";
    readonly path: undefined;
    request?: never | undefined;
    readonly kind: "procedure";
    responses: InferredResponses<...>;
    output: "response";
}>
getFirst
>;
type
type GetFirstTodoSuccess = {
    status: 200;
    body: {
        readonly id: "todo_1";
        readonly title: "Write docs";
        readonly code?: undefined;
    };
    headers: Headers;
}
GetFirstTodoSuccess
=
type InferClientSuccess<T extends RouteTree> = T extends {
    readonly "~restrpc": infer TRoute extends RouteDeclaration;
} ? ClientSuccessForDeclaration<TRoute> : { [K in keyof T]: T[K] extends RouteTree ? InferClientSuccess<T[K]> : never; }
Infers the successful result of a route's client call.
@remarksRoutes declared with `.response()` produce the envelopes of their 2xx responses. Routes declared with `.output()` produce their output directly. Pass a route tree to infer a matching tree of results.@see{@link https://rest-rpc.dev/docs/client/fetch-client#type-helpers}
InferClientSuccess
<typeof
const getFirst: HandlerImplementation<{
    readonly kind: "procedure";
    readonly method: "POST";
    readonly path: undefined;
    request?: never | undefined;
    responses: {};
}, ExpressHandlerFields, DefaultContext, {
    readonly status: 200;
    readonly body: {
        readonly id: "todo_1";
        readonly title: "Write docs";
        readonly code?: undefined;
    };
} | {
    readonly status: 404;
    readonly body: {
        readonly code: "TODO_NOT_FOUND";
        readonly id?: undefined;
        readonly title?: undefined;
    };
}, {
    readonly method: "POST";
    readonly path: undefined;
    request?: never | undefined;
    readonly kind: "procedure";
    responses: InferredResponses<...>;
    output: "response";
}>
getFirst
>;
type
type GetFirstTodoError = {
    status: 404;
    body: {
        readonly code: "TODO_NOT_FOUND";
        readonly id?: undefined;
        readonly title?: undefined;
    };
    headers: Headers;
}
GetFirstTodoError
=
type InferClientError<T extends RouteTree> = T extends {
    readonly "~restrpc": infer TRoute extends RouteDeclaration;
} ? ClientErrorForDeclaration<TRoute> : { [K in keyof T]: T[K] extends RouteTree ? InferClientError<T[K]> : never; }
Infers the declared error responses a route's client call can return.
@remarksIncludes the envelopes of declared non-2xx responses. Routes declared with `.output()` infer `never` because the client throws their non-2xx responses. Thrown `HttpError` and `Error` values are not included. Pass a route tree to infer a matching tree of types.@see{@link https://rest-rpc.dev/docs/client/fetch-client#type-helpers}
InferClientError
<typeof
const getFirst: HandlerImplementation<{
    readonly kind: "procedure";
    readonly method: "POST";
    readonly path: undefined;
    request?: never | undefined;
    responses: {};
}, ExpressHandlerFields, DefaultContext, {
    readonly status: 200;
    readonly body: {
        readonly id: "todo_1";
        readonly title: "Write docs";
        readonly code?: undefined;
    };
} | {
    readonly status: 404;
    readonly body: {
        readonly code: "TODO_NOT_FOUND";
        readonly id?: undefined;
        readonly title?: undefined;
    };
}, {
    readonly method: "POST";
    readonly path: undefined;
    request?: never | undefined;
    readonly kind: "procedure";
    responses: InferredResponses<...>;
    output: "response";
}>
getFirst
>;
const
const events: HandlerImplementation<{
    readonly kind: "http";
    readonly method: "GET";
    readonly path: "/todos/events";
    request?: never | undefined;
    responses: {
        200: {
            kind: "stream";
            body: z.ZodObject<{
                id: z.ZodString;
                message: z.ZodString;
            }, z.core.$strip>;
        };
    };
    readonly output: "output";
}, ExpressHandlerFields, DefaultContext, AsyncGenerator<{
    id: string;
    message: string;
}, void, unknown>, {
    readonly kind: "http";
    readonly method: "GET";
    readonly path: "/todos/events";
    request?: never | undefined;
    responses: {
        200: {
            kind: "stream";
            body: z.ZodObject<{
                id: z.ZodString;
                message: z.ZodString;
            }, z.core.$strip>;
        };
    };
    readonly output: "output";
}>
events
= const route: ServerRouteBuilder<ExpressHandlerFields, DefaultContext>
Entry point for declaring routes with handlers for Express.
@remarksResponses are inferred from the handler unless they are declared before `.handler()`.@see{@link https://rest-rpc.dev/docs/quickstart#define-and-register-routes}@see{@link https://rest-rpc.dev/docs/server/express#framework-context}
route
.get<"/todos/events", never>(this: BuilderReceiver<BuilderPath, never>, path?: "/todos/events" | undefined): RouteBuilderView<WithHttpRoute<DerivedState, "GET">, ServerBuilderExtension<ExpressHandlerFields, DefaultContext>, "/todos/events", never>get("/todos/events") .
streamOutput<z.ZodObject<{
    id: z.ZodString;
    message: z.ZodString;
}, z.core.$strip>, "/todos/events", never>(this: BuilderReceiver<"/todos/events", never>, schema: z.ZodObject<{
    id: z.ZodString;
    message: z.ZodString;
}, z.core.$strip>): RouteBuilderView<WithPlainOutput<UseMethod<WithHttpRoute<DerivedState, "GET">, "output">, {
    kind: "stream";
    body: z.ZodObject<{
        id: z.ZodString;
        message: z.ZodString;
    }, z.core.$strip>;
}>, ServerBuilderExtension<ExpressHandlerFields, DefaultContext>, "/todos/events", never>
Declares a streaming plain output.
@see{@link https://rest-rpc.dev/docs/streaming}
streamOutput
(import zz.
function object<{
    id: z.ZodString;
    message: z.ZodString;
}>(shape?: {
    id: z.ZodString;
    message: z.ZodString;
} | undefined, params?: string | {
    error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueInvalidType<unknown> | z.core.$ZodIssueUnrecognizedKeys>> | undefined;
    message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
    id: z.ZodString;
    message: z.ZodString;
}, z.core.$strip>
object
({ id: z.ZodStringid: import zz.function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)string(), message: z.ZodStringmessage: import zz.function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)string() }))
.
handler<AsyncGenerator<{
    id: string;
    message: string;
}, void, unknown>>(handler: HandlerFor<{
    readonly kind: "http";
    readonly method: "GET";
    readonly path: "/todos/events";
    request?: never | undefined;
    responses: {
        200: {
            kind: "stream";
            body: z.ZodObject<{
                id: z.ZodString;
                message: z.ZodString;
            }, z.core.$strip>;
        };
    };
    readonly output: "output";
}, ExpressHandlerFields, DefaultContext, AsyncGenerator<{
    id: string;
    message: string;
}, void, unknown>>): HandlerImplementation<{
    readonly kind: "http";
    readonly method: "GET";
    readonly path: "/todos/events";
    request?: never | undefined;
    responses: {
        200: {
            kind: "stream";
            body: z.ZodObject<{
                id: z.ZodString;
                message: z.ZodString;
            }, z.core.$strip>;
        };
    };
    readonly output: "output";
}, ExpressHandlerFields, DefaultContext, AsyncGenerator<...>, {
    readonly kind: "http";
    readonly method: "GET";
    readonly path: "/todos/events";
    request?: never | undefined;
    responses: {
        200: {
            kind: "stream";
            body: z.ZodObject<{
                id: z.ZodString;
                message: z.ZodString;
            }, z.core.$strip>;
        };
    };
    readonly output: "output";
}>
handler
(async function* () {
yield { id: stringid: "todo_1", message: stringmessage: "Created" }; }); type
type TodoEvent = {
    id: string;
    message: string;
}
TodoEvent
=
type InferClientStreamData<T extends RouteTree> = T extends {
    readonly "~restrpc": infer TRoute extends RouteDeclaration;
} ? ClientStreamDataForDeclaration<TRoute> : { [K in keyof T]: T[K] extends RouteTree ? InferClientStreamData<T[K]> : never; }
Infers the `data` of each event received from a streaming route.
@remarksUnwraps the `SseEvent` values of successful stream responses. Routes without a stream response infer `never`. Pass a route tree to infer a matching tree of event data types.@see{@link https://rest-rpc.dev/docs/client/fetch-client#type-helpers}@see{@link https://rest-rpc.dev/docs/client/fetch-client#consume-streams}
InferClientStreamData
<typeof
const events: HandlerImplementation<{
    readonly kind: "http";
    readonly method: "GET";
    readonly path: "/todos/events";
    request?: never | undefined;
    responses: {
        200: {
            kind: "stream";
            body: z.ZodObject<{
                id: z.ZodString;
                message: z.ZodString;
            }, z.core.$strip>;
        };
    };
    readonly output: "output";
}, ExpressHandlerFields, DefaultContext, AsyncGenerator<{
    id: string;
    message: string;
}, void, unknown>, {
    readonly kind: "http";
    readonly method: "GET";
    readonly path: "/todos/events";
    request?: never | undefined;
    responses: {
        200: {
            kind: "stream";
            body: z.ZodObject<{
                id: z.ZodString;
                message: z.ZodString;
            }, z.core.$strip>;
        };
    };
    readonly output: "output";
}>
events
>;
Type Infers
InferClientRequest<Route, GlobalHeaders> The request passed to the route call, or undefined for routes without input.
InferClientResponse<Route> Every declared response, or the output of a .output() route.
InferClientSuccess<Route> The declared 2xx responses, or the output of a .output() route.
InferClientError<Route> The declared non-2xx responses. .output() routes infer never.
InferClientStreamData<Route> The data of each event from a streaming route, without SseEvent.

InferClientError contains only the responses the client returns. The HttpError and Error values it throws are not included.

InferClientRequest accepts the type of your globalHeaders as a second argument, so headers that globalHeaders always provides become optional:

import type { InferClientRequest } from "@rest-rpc/core";
import type { api } from "./client-contract";

const globalHeaders = { authorization: () => `Bearer ${getToken()}` };

type UpdateTodoRequest = InferClientRequest<
	typeof api.todos.update,
	typeof globalHeaders
>;

ApiClientFor<typeof api> is the type of the whole client created from a contract.

Each helper also accepts a route tree and infers a matching tree of types, so one exported type covers every route:

import type { InferClientResponse } from "@rest-rpc/core";
import type { api } from "./client-contract";

export type ClientResponses = InferClientResponse<typeof api>;

type TodoResponse = ClientResponses["todos"]["get"];

Was this page helpful?