httpLink
Queries as GET, mutations as POST and subscriptions as SSE, with batching, streamed outputs and files.
On this page
httpLink sends queries as GET, mutations as POST and subscriptions as server-sent events, to a handler's URL:
import { function createTrpcClient<TSource, const TLinks extends ReadonlyArray<TrpcLink<any, RouterOf<TSource>>>>(opts: TypedClientOptions<TSource, TLinks> & DuplicateSwitchCheck<TLinks> & UnmetNeedCheck<TLinks>): TrpcClient<RouterOf<TSource>, LinksContext<TLinks>, undefined, LinksTypes<TLinks>>createTrpcClient, function httpLink<TOpts extends HttpLinkOptions>(opts: TOpts): TrpcLink<HttpLinkDecl<TOpts>>httpLink, function routerType<TRouter>(): RouterType<TRouter>routerType } from '@trpc12/trpc/client';
const const client: TrpcClient<{
post: {
byId: Procedure<{
type: "query";
ctx: Context;
meta: {};
input: {
id: string;
};
output: Post;
errors: TrpcError<"NOT_FOUND", {
id: string;
}> | SchemaValidationError<ZodObject<{
id: ZodString;
}, $strip>>;
services: never;
}>;
list: Procedure<{
type: "query";
ctx: Context;
meta: {};
input: undefined;
output: Post[];
errors: never;
services: never;
}>;
create: Procedure<{
type: "mutation";
ctx: Context;
meta: {};
input: {
title: string;
};
output: Post;
errors: TrpcError<...> | SchemaValidationError<...>;
services: never;
}>;
};
}, {}, undefined, LinksTypes<...>>
client = createTrpcClient<RouterType<{
post: {
byId: Procedure<{
type: "query";
ctx: Context;
meta: {};
input: {
id: string;
};
output: Post;
errors: TrpcError<"NOT_FOUND", {
id: string;
}> | SchemaValidationError<ZodObject<{
id: ZodString;
}, $strip>>;
services: never;
}>;
list: Procedure<{
type: "query";
ctx: Context;
meta: {};
input: undefined;
output: Post[];
errors: never;
services: never;
}>;
create: Procedure<{
type: "mutation";
ctx: Context;
meta: {};
input: {
title: string;
};
output: Post;
errors: TrpcError<...> | SchemaValidationError<...>;
services: never;
}>;
};
}>, readonly [...]>(opts: TypedClientOptions<...>): TrpcClient<...>
createTrpcClient({
TypedClientOptions<RouterType<{ post: { byId: Procedure<{ type: "query"; ctx: Context; meta: {}; input: { id: string; }; output: Post; errors: TrpcError<"NOT_FOUND", { id: string; }> | SchemaValidationError<ZodObject<{ id: ZodString; }, $strip>>; services: never; }>; list: Procedure<...>; create: Procedure<...>; }; }>, readonly [...]>.router: RouterType<{
post: {
byId: Procedure<{
type: "query";
ctx: Context;
meta: {};
input: {
id: string;
};
output: Post;
errors: TrpcError<"NOT_FOUND", {
id: string;
}> | SchemaValidationError<ZodObject<{
id: ZodString;
}, $strip>>;
services: never;
}>;
list: Procedure<{
type: "query";
ctx: Context;
meta: {};
input: undefined;
output: Post[];
errors: never;
services: never;
}>;
create: Procedure<{
type: "mutation";
ctx: Context;
meta: {};
input: {
title: string;
};
output: Post;
errors: TrpcError<...> | SchemaValidationError<...>;
services: never;
}>;
};
}>
router: routerType<{
post: {
byId: Procedure<{
type: "query";
ctx: Context;
meta: {};
input: {
id: string;
};
output: Post;
errors: TrpcError<"NOT_FOUND", {
id: string;
}> | SchemaValidationError<ZodObject<{
id: ZodString;
}, $strip>>;
services: never;
}>;
list: Procedure<{
type: "query";
ctx: Context;
meta: {};
input: undefined;
output: Post[];
errors: never;
services: never;
}>;
create: Procedure<{
type: "mutation";
ctx: Context;
meta: {};
input: {
title: string;
};
output: Post;
errors: TrpcError<...> | SchemaValidationError<...>;
services: never;
}>;
};
}>(): RouterType<...>
routerType<type AppRouter = {
post: {
byId: Procedure<{
type: "query";
ctx: Context;
meta: {};
input: {
id: string;
};
output: Post;
errors: TrpcError<"NOT_FOUND", {
id: string;
}> | SchemaValidationError<ZodObject<{
id: ZodString;
}, $strip>>;
services: never;
}>;
list: Procedure<{
type: "query";
ctx: Context;
meta: {};
input: undefined;
output: Post[];
errors: never;
services: never;
}>;
create: Procedure<{
type: "mutation";
ctx: Context;
meta: {};
input: {
title: string;
};
output: Post;
errors: TrpcError<...> | SchemaValidationError<...>;
services: never;
}>;
};
}
AppRouter>(),
TypedClientOptions<RouterType<{ post: { byId: Procedure<{ type: "query"; ctx: Context; meta: {}; input: { id: string; }; output: Post; errors: TrpcError<"NOT_FOUND", { id: string; }> | SchemaValidationError<ZodObject<{ id: ZodString; }, $strip>>; services: never; }>; list: Procedure<...>; create: Procedure<...>; }; }>, readonly [...]>.links: readonly [TrpcLink<HttpLinkDecl<{
url: string;
batch: true;
}>, any>]
links: [httpLink<{
url: string;
batch: true;
}>(opts: {
url: string;
batch: true;
}): TrpcLink<HttpLinkDecl<{
url: string;
batch: true;
}>, any>
httpLink({ url: stringurl: 'http://localhost:3000/trpc', batch: truebatch: true })],
});url: the handler's endpoint.batch: send the queries made in the same tick as one request, and the mutations as another (below).headers: headers for every request, or a function of the calls a request carries. A call'sheadersoption is merged in.init:fetchoptions (below).fetch: afetchof your own.serializer: must match the handler's, and client types follow it (Serializers).maxUrlLengthandlongQuery: when a query goes with a body (below).binary: streamed outputs as binary frames (below).subscriptions: false: no SSE, so procedures lose.subscribe(), for a link that only carries queries and mutations.reconnect: how a dropped subscription reconnects,{ attempts: 5, delayMs }by default.
Batching
With batch: true, the queries made in the same tick go as one request, and so do the mutations, up to maxItems calls a request (batch: { maxItems }, 10 by default). The handler has to accept batches too, with createFetchHandler({ batch: true }). A call opts out with { batch: false }, and calls with different inits are batched apart.
The response has a line per call, with its own status, so each call settles when its line arrives and one call's error doesn't fail the others.
Request options
init sets fetch options. The link's init is a default, or a function called once per HTTP request with the calls it carries; a call's init is merged onto it (shallowly, but headers and next key by key). next passes Next's data-cache options through:
httpLink<{
url: string;
init: () => {
cache: "no-store";
headers: HeadersInit;
};
}>(opts: {
url: string;
init: () => {
cache: "no-store";
headers: HeadersInit;
};
}): TrpcLink<{
form: JsonForm;
provides: "response";
}, any>
httpLink({
url: stringurl,
init: () => {
cache: "no-store";
headers: HeadersInit;
}
init: () => ({ cache: "no-store"cache: 'no-store', headers: HeadersInitheaders: function forwardCookies(): HeadersInitforwardCookies() }),
});
await const client: TrpcClient<{
post: {
byId: Procedure<{
type: "query";
ctx: Context;
meta: {};
input: {
id: string;
};
output: Post;
errors: TrpcError<"NOT_FOUND", {
id: string;
}> | SchemaValidationError<ZodObject<{
id: ZodString;
}, $strip>>;
services: never;
}>;
list: Procedure<{
type: "query";
ctx: Context;
meta: {};
input: undefined;
output: Post[];
errors: never;
services: never;
}>;
create: Procedure<{
type: "mutation";
ctx: Context;
meta: {};
input: {
title: string;
};
output: Post;
errors: TrpcError<...> | SchemaValidationError<...>;
services: never;
}>;
};
}, {}, undefined, LinksTypes<...>>
client.post: ClientRouter<{
byId: Procedure<{
type: "query";
ctx: Context;
meta: {};
input: {
id: string;
};
output: Post;
errors: TrpcError<"NOT_FOUND", {
id: string;
}> | SchemaValidationError<ZodObject<{
id: ZodString;
}, $strip>>;
services: never;
}>;
list: Procedure<{
type: "query";
ctx: Context;
meta: {};
input: undefined;
output: Post[];
errors: never;
services: never;
}>;
create: Procedure<{
type: "mutation";
ctx: Context;
meta: {};
input: {
title: string;
};
output: Post;
errors: TrpcError<...> | SchemaValidationError<...>;
services: never;
}>;
}, {}, undefined, LinksTypes<...>>
post.list: {
query(input?: undefined, opts?: CallOptions<{}> | undefined): TrpcPromise<{
id: string;
title: string;
author: string | null;
createdAt: string;
}[], CallError<{
type: "query";
ctx: Context;
meta: {};
input: undefined;
output: Post[];
errors: never;
services: never;
}, JsonForm, never>>;
}
list.function query(input?: undefined, opts?: CallOptions<{}> | undefined): TrpcPromise<{
id: string;
title: string;
author: string | null;
createdAt: string;
}[], CallError<{
type: "query";
ctx: Context;
meta: {};
input: undefined;
output: Post[];
errors: never;
services: never;
}, JsonForm, never>>
query(const input: undefinedinput, {
init?: TrpcRequestInit | undefinedinit: { next?: {
revalidate?: number | false;
tags?: string[];
} | undefined
next: { revalidate?: number | false | undefinedrevalidate: 60, tags?: string[] | undefinedtags: ['posts'] } },
});Each call's response reaches an onResponse call option ({ status, headers }) before its value goes up the chain. A batched call gets its own line's status and the batch's headers (Custom links).
Query URLs
Query inputs go in the URL as ?input=<JSON>: ?input={"post":{"id":"1"}}, URL-encoded. Object keys are sorted (array order is kept), so equal inputs make the same URL and hit the same HTTP cache entry. A serializer that makes refs adds &refs=.
A query whose URL would be longer than maxUrlLength (2048) is sent with a body. That is POST by default; longQuery: 'query' uses the HTTP QUERY method, a safe method with a body that caches can key on. Every proxy on the way has to allow QUERY.
Streamed outputs and files
Outputs with nested promises, async iterables or files stream as JSONL (Streaming). With binary: true they stream as binary frames instead, which carry file bytes unencoded; a server that doesn't know frames answers with JSONL. An unbatched call whose output is a File or Blob gets it as the raw response body.
Inputs holding a File or Blob go alone as multipart/form-data (Files).
Subscriptions
A subscription is an SSE request. When the connection drops, the link reconnects up to reconnect.attempts times and sends the last tracked() event id, so the stream resumes where it stopped. A call made with { reconnect: false } ends with NETWORK_ERROR instead.