HTTP Batch Stream Link
A terminating link that batches tRPC calls and streams each response as soon as it is ready
On this page
httpBatchStreamLink is a terminating link that batches an array of individual tRPC operations into a single HTTP request (equivalent to httpBatchLink), but doesn't wait for all the responses of the batch to be ready and streams the responses as soon as any data is available.
Options
Options are identical to httpBatchLink options, with the following addition:
| Option | Type | Default | Description |
|---|---|---|---|
streamHeader | 'trpc-accept' | 'accept' | 'trpc-accept' | Which header to use to signal the server that the client wants a streaming response. 'accept' uses the standard Accept header instead of the custom trpc-accept header, which can avoid CORS preflight for cross-origin streaming queries since Accept is a CORS-safelisted header. |
Usage
All usage and options are identical to
httpBatchLink.
You can import and add the httpBatchStreamLink to the links array as such:
import { function createTRPCClient<TRouter extends AnyRouter>(opts: CreateTRPCClientOptions<TRouter>): TRPCClient<TRouter>createTRPCClient, function httpBatchStreamLink<TRouter extends AnyRouter>(opts: HTTPBatchStreamLinkOptions<TRouter["_def"]["_config"]["$types"]>): TRPCLink<TRouter>httpBatchStreamLink } from '@trpc/client';
import type { type AppRouter = Router<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>> & DecorateCreateRouterOptions<{}>
AppRouter } from './server';
const const client: TRPCClient<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>
client = createTRPCClient<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>(opts: CreateTRPCClientOptions<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>): TRPCClient<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<...>>>
createTRPCClient<type AppRouter = Router<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>> & DecorateCreateRouterOptions<{}>
AppRouter>({
links: TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>[]
links: [
httpBatchStreamLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>(opts: HTTPBatchStreamLinkOptions<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}>): TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>
httpBatchStreamLink({
url: string | URLurl: 'http://localhost:3000',
}),
],
});After that, calls made in the same event loop tick are batched together, so you don't need Promise.all, though it's a common way to do it. The code below will produce exactly one HTTP request, and the server will call createContext once for it:
const const somePosts: [{
id: number;
title: string;
}, {
id: number;
title: string;
}, {
id: number;
title: string;
}]
somePosts = await var Promise: PromiseConstructorPromise.PromiseConstructor.all<[TRPCTaggedPromise<{
id: number;
title: string;
}, DefaultErrorShape>, TRPCTaggedPromise<{
id: number;
title: string;
}, DefaultErrorShape>, TRPCTaggedPromise<{
id: number;
title: string;
}, DefaultErrorShape>]>(values: [TRPCTaggedPromise<{
id: number;
title: string;
}, DefaultErrorShape>, TRPCTaggedPromise<{
id: number;
title: string;
}, DefaultErrorShape>, TRPCTaggedPromise<...>]): Promise<...> (+1 overload)
all([
const trpc: TRPCClient<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: number;
output: {
id: number;
title: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
}>>>
trpc.post: DecoratedProcedureRecord<{
transformer: false;
errorShape: DefaultErrorShape;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: number;
output: {
id: number;
title: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>
post.byId: {
query: Resolver<{
input: number;
output: {
id: number;
title: string;
};
errorShape: DefaultErrorShape;
transformer: false;
}>;
}
byId.query: (input: number, opts?: TRPCProcedureOptions) => TRPCTaggedPromise<{
id: number;
title: string;
}, DefaultErrorShape>
query(1),
const trpc: TRPCClient<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: number;
output: {
id: number;
title: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
}>>>
trpc.post: DecoratedProcedureRecord<{
transformer: false;
errorShape: DefaultErrorShape;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: number;
output: {
id: number;
title: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>
post.byId: {
query: Resolver<{
input: number;
output: {
id: number;
title: string;
};
errorShape: DefaultErrorShape;
transformer: false;
}>;
}
byId.query: (input: number, opts?: TRPCProcedureOptions) => TRPCTaggedPromise<{
id: number;
title: string;
}, DefaultErrorShape>
query(2),
const trpc: TRPCClient<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: number;
output: {
id: number;
title: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
}>>>
trpc.post: DecoratedProcedureRecord<{
transformer: false;
errorShape: DefaultErrorShape;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: number;
output: {
id: number;
title: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>
post.byId: {
query: Resolver<{
input: number;
output: {
id: number;
title: string;
};
errorShape: DefaultErrorShape;
transformer: false;
}>;
}
byId.query: (input: number, opts?: TRPCProcedureOptions) => TRPCTaggedPromise<{
id: number;
title: string;
}, DefaultErrorShape>
query(3),
]);Each procedure still runs its own resolver, so whether this makes one database query depends on your data layer. Prisma batches findUnique() calls made in the same tick into one query, and a DataLoader can do the same for other queries. Otherwise, each resolver makes its own query.
Streaming mode
When batching requests together, the behavior of a regular httpBatchLink is to wait for all requests to finish before sending the response. If you want to send responses as soon as they are ready, you can use httpBatchStreamLink instead. This is useful for long-running requests.
import { function createTRPCClient<TRouter extends AnyRouter>(opts: CreateTRPCClientOptions<TRouter>): TRPCClient<TRouter>createTRPCClient, function httpBatchStreamLink<TRouter extends AnyRouter>(opts: HTTPBatchStreamLinkOptions<TRouter["_def"]["_config"]["$types"]>): TRPCLink<TRouter>httpBatchStreamLink } from '@trpc/client';
import type { type AppRouter = Router<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>> & DecorateCreateRouterOptions<{}>
AppRouter } from './server';
const const client: TRPCClient<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>
client = createTRPCClient<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>(opts: CreateTRPCClientOptions<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>): TRPCClient<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<...>>>
createTRPCClient<type AppRouter = Router<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>> & DecorateCreateRouterOptions<{}>
AppRouter>({
links: TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>[]
links: [
httpBatchStreamLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>(opts: HTTPBatchStreamLinkOptions<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}>): TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>
httpBatchStreamLink({
url: string | URLurl: 'http://localhost:3000',
}),
],
});Compared to a regular httpBatchLink, a httpBatchStreamLink will:
- Cause the requests to be sent with a
trpc-accept: application/jsonlheader (orAccept: application/jsonlwhen usingstreamHeader: 'accept') - Cause the response to be sent with
transfer-encoding: chunkedandcontent-type: application/jsonby default. You can set the streamed responsecontent-typetoapplication/jsonl,application/x-ndjson, or another value on the server viainitTRPC.create({ jsonl: { contentType: ... } }). - Remove the
datakey from the argument object passed toresponseMeta(because with a streamed response, the headers are sent before the data is available)
Async generators and deferred promises
You can try this out on the homepage of tRPC.io: https://trpc.io/?try=minimal#try-it-out
import { function createTRPCClient<TRouter extends AnyRouter>(opts: CreateTRPCClientOptions<TRouter>): TRPCClient<TRouter>createTRPCClient, function httpBatchStreamLink<TRouter extends AnyRouter>(opts: HTTPBatchStreamLinkOptions<TRouter["_def"]["_config"]["$types"]>): TRPCLink<TRouter>httpBatchStreamLink } from '@trpc/client';
import type { type AppRouter = Router<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
examples: {
iterable: QueryProcedure<{
input: void;
output: AsyncGenerator<number, void, unknown>;
meta: object;
errorShape: DefaultErrorShape;
}>;
};
}>> & DecorateCreateRouterOptions<{
examples: {
iterable: QueryProcedure<{
input: void;
output: AsyncGenerator<number, void, unknown>;
meta: object;
errorShape: DefaultErrorShape;
}>;
};
}>
AppRouter } from './server';
const const trpc: TRPCClient<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
examples: {
iterable: QueryProcedure<{
input: void;
output: AsyncGenerator<number, void, unknown>;
meta: object;
errorShape: DefaultErrorShape;
}>;
};
}>>>
trpc = createTRPCClient<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
examples: {
iterable: QueryProcedure<{
input: void;
output: AsyncGenerator<number, void, unknown>;
meta: object;
errorShape: DefaultErrorShape;
}>;
};
}>>>(opts: CreateTRPCClientOptions<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
examples: {
iterable: QueryProcedure<{
input: void;
output: AsyncGenerator<number, void, unknown>;
meta: object;
errorShape: DefaultErrorShape;
}>;
};
}>>>): TRPCClient<...>
createTRPCClient<type AppRouter = Router<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
examples: {
iterable: QueryProcedure<{
input: void;
output: AsyncGenerator<number, void, unknown>;
meta: object;
errorShape: DefaultErrorShape;
}>;
};
}>> & DecorateCreateRouterOptions<{
examples: {
iterable: QueryProcedure<{
input: void;
output: AsyncGenerator<number, void, unknown>;
meta: object;
errorShape: DefaultErrorShape;
}>;
};
}>
AppRouter>({
links: TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
examples: {
iterable: QueryProcedure<{
input: void;
output: AsyncGenerator<number, void, unknown>;
meta: object;
errorShape: DefaultErrorShape;
}>;
};
}>>>[]
links: [
httpBatchStreamLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
examples: {
iterable: QueryProcedure<{
input: void;
output: AsyncGenerator<number, void, unknown>;
meta: object;
errorShape: DefaultErrorShape;
}>;
};
}>>>(opts: HTTPBatchStreamLinkOptions<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}>): TRPCLink<...>
httpBatchStreamLink({
url: string | URLurl: 'http://localhost:3000',
}),
],
});
const iterable = await const trpc: TRPCClient<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
examples: {
iterable: QueryProcedure<{
input: void;
output: AsyncGenerator<number, void, unknown>;
meta: object;
errorShape: DefaultErrorShape;
}>;
};
}>>>
trpc.examples: DecoratedProcedureRecord<{
transformer: false;
errorShape: DefaultErrorShape;
}, DecorateCreateRouterOptions<{
iterable: QueryProcedure<{
input: void;
output: AsyncGenerator<number, void, unknown>;
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>
examples.iterable: {
query: Resolver<{
input: void;
output: AsyncIterable<number, never, unknown>;
errorShape: DefaultErrorShape;
transformer: false;
}>;
}
iterable.query: (input: void, opts?: TRPCProcedureOptions) => TRPCTaggedPromise<AsyncIterable<number, never, unknown>, DefaultErrorShape>query();
for await (const const value: numbervalue of const iterable: AsyncIterable<number, never, unknown>iterable) {
var console: Consoleconsole.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)log('Iterable:', value);}Compatibility (client-side)
Browsers
Browser support should be identical to fetch support.
Node.js / Deno
For runtimes other than the browser ones, the fetch implementation should support streaming, meaning that the response obtained by await fetch(...) should have a body property of type ReadableStream<Uint8Array> | NodeJS.ReadableStream, meaning that:
- either
response.body.getReaderis a function that returns aReadableStreamDefaultReader<Uint8Array>object - or
response.bodyis aUint8ArrayBuffer
This includes support for undici, node-fetch, native Node.js fetch implementation, and WebAPI fetch implementation (browsers).
React Native
Receiving the stream relies on the TextDecoder and TextDecoderStream APIs, which are not available in React Native. It's important to note that if your TextDecoderStream polyfill does not automatically polyfill ReadableStream and WritableStream those will also need to be polyfilled. If you still want to enable streaming, you need to polyfill those.
You will also need to override the default fetch in the httpBatchStreamLink configuration options with one that can stream. In the below example we will be using react-native-fetch-api, which streams text when you pass its reactNative option. On Expo, you can pass fetch from expo/fetch instead, which streams without extra options.
import { httpBatchStreamLink } from '@trpc/client';
import { fetch } from 'react-native-fetch-api';
httpBatchStreamLink({
fetch: (url, opts) =>
fetch(url, {
...opts,
reactNative: { textStreaming: true },
}),
url: 'http://localhost:3000',
});Compatibility (server-side)
Reference
You can check out the source code for this link on GitHub.
Configure JSONL streaming options
When setting up your root config, you can pass in a jsonl option to configure ping behavior and the response content-type for streamed batch responses.
import { const initTRPC: TRPCBuilder<object, object>initTRPC } from '@trpc/server';
const const t: TRPCRootObject<object, object, {
jsonl: {
pingMs: number;
contentType: "application/jsonl";
};
}, {
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}>
t = const initTRPC: TRPCBuilder<object, object>initTRPC.TRPCBuilder<object, object>.create<{
jsonl: {
pingMs: number;
contentType: "application/jsonl";
};
}>(opts?: {
jsonl: {
pingMs: number;
contentType: "application/jsonl";
};
} | undefined): TRPCRootObject<object, object, {
jsonl: {
pingMs: number;
contentType: "application/jsonl";
};
}, {
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}>
create({
jsonl: {
pingMs: number;
contentType: "application/jsonl";
}
jsonl: {
pingMs: numberpingMs: 1000,
contentType: "application/jsonl"contentType: 'application/jsonl',
},
});The default contentType remains application/json for backwards compatibility. Use application/jsonl if you want the response header to match the trpc-accept / Accept header sent by httpBatchStreamLink, or choose another value that works better with your tooling.