Serving v11 clients
createV11Handler serves the new router to @trpc/client 11 apps on the endpoint they already call, in v11's wire format and error shape.
Move the server first. createV11Handler from @trpc12/trpc/interop serves the new router to @trpc/client 11 apps on the endpoint they already call, so you can deploy before every client (a mobile app in the stores, a tab left open) updates. Serve the v12 protocol next to it, on another endpoint:
import class superjsonsuperjson from 'superjson';
import { function createV11Handler<TRouter extends AnyRouter, TPlatform = undefined>(opts: V11HandlerOptions<TRouter, TPlatform>): V11Handler<TPlatform>createV11Handler } from '@trpc12/trpc/interop';
import { function createFetchHandler<TRouter extends AnyRouter, TPlatform = undefined>(opts: FetchHandlerOptions<TRouter, TPlatform>): FetchHandler<TPlatform>createFetchHandler } from '@trpc12/trpc/server';
// Old apps: @trpc/client 11, on /trpc, with the transformer they were built with.
const const legacy: V11Handler<undefined>legacy = createV11Handler<{
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>(opts: V11HandlerOptions<...>): V11Handler<...>
createV11Handler({
BaseV11HandlerOptions<{ 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<...>; }; }>.router: {
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: const 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,
createContext: (opts: CreateContextOpts<undefined>) => MaybePromise<{
user: User | null;
}>
createContext,
BaseV11HandlerOptions<{ 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<...>; }; }>.transformer?: V11TransformerOption | undefinedtransformer: class superjsonsuperjson,
});
// New clients: the v12 protocol, on /rpc.
const const handler: FetchHandler<undefined>handler = createFetchHandler<{
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>(opts: FetchHandlerOptions<...>): FetchHandler<...>
createFetchHandler({
BaseHandlerOptions<{ 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<...>; }; }>.router: {
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: const 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,
createContext: (opts: CreateContextOpts<undefined>) => MaybePromise<{
user: User | null;
}>
createContext,
BaseHandlerOptions<TRouter extends AnyRouter>.endpoint?: string | undefinedendpoint: '/rpc',
});
export default {
fetch: (request: Request) => Promise<Response>fetch: (request: Requestrequest: Request) =>
new var URL: new (url: string | URL, base?: string | URL) => URLURL(request: Requestrequest.Request.url: stringurl).URL.pathname: stringpathname.String.startsWith(searchString: string, position?: number): booleanstartsWith('/trpc/')
? const legacy: V11Handler<undefined>legacy.V11Handler<undefined>.fetch: (request: Request) => Promise<Response>fetch(request: Requestrequest)
: const handler: FetchHandler<undefined>handler.FetchHandler<undefined>.fetch: (request: Request) => Promise<Response>fetch(request: Requestrequest),
};Under serve() from @trpc12/trpc/http, the v11 handler runs as its own Effect HTTP app, and WebSocket upgrades on its endpoint reach legacy.websocket().
It speaks every v11 link:
| v11 link | Served |
|---|---|
httpLink, httpBatchLink | GET queries, POST mutations, ?batch=1 batches (at most 100 calls by default: batch: { maxItems }, or batch: false), 207 when a batch's statuses differ |
httpBatchStreamLink | JSON Lines: each call's result as it settles, and promises and async iterables at the root or a top-level key of an output |
httpSubscriptionLink | SSE with tracked() ids, ping, return and serialized-error events; a reconnect's lastEventId (in the input, as v11 sends it) reaches the resolver's lastEventId |
wsLink | queries, mutations, subscriptions with subscription.stop, connectionParams, PING/PONG |
FormData and octet-stream | a form becomes an object (bracket notation, files as Files); bytes become a Blob |
Errors go out in v11's shape, so TRPCClientError reads them: { message, code: -32004, data: { code: 'NOT_FOUND', httpStatus: 404, path } }. A code v11 doesn't have (POST_LOCKED, declared with status 423) takes v11's code for its status, INTERNAL_SERVER_ERROR when there's none; the HTTP status stays. If the v11 router had an errorFormatter, reproduce it:
createV11Handler<{
post: {
list: Procedure<{
type: "query";
ctx: {};
meta: {};
input: undefined;
output: Post[];
errors: never;
services: never;
}>;
};
}, undefined>(opts: V11HandlerOptions<{
post: {
list: Procedure<{
type: "query";
ctx: {};
meta: {};
input: undefined;
output: Post[];
errors: never;
services: never;
}>;
};
}, undefined>): V11Handler<undefined>
createV11Handler({
BaseV11HandlerOptions<{ post: { list: Procedure<{ type: "query"; ctx: {}; meta: {}; input: undefined; output: Post[]; errors: never; services: never; }>; }; }>.router: {
post: {
list: Procedure<{
type: "query";
ctx: {};
meta: {};
input: undefined;
output: Post[];
errors: never;
services: never;
}>;
};
}
router: const appRouter: {
post: {
list: Procedure<{
type: "query";
ctx: {};
meta: {};
input: undefined;
output: Post[];
errors: never;
services: never;
}>;
};
}
appRouter,
BaseV11HandlerOptions<{ post: { list: Procedure<{ type: "query"; ctx: {}; meta: {}; input: undefined; output: Post[]; errors: never; services: never; }>; }; }>.errorFormatter?: ((opts: V11ErrorFormatterOpts) => V11ErrorShape) | undefinederrorFormatter: (opts: V11ErrorFormatterOptsopts) => ({
...opts: V11ErrorFormatterOptsopts.V11ErrorFormatterOpts.shape: V11ErrorShapeshape,
V11ErrorShape.data: {
readonly [key: string]: unknown;
readonly code: string;
readonly httpStatus: number;
readonly path?: string;
}
data: {
...opts: V11ErrorFormatterOptsopts.V11ErrorFormatterOpts.shape: V11ErrorShapeshape.V11ErrorShape.data: {
readonly [key: string]: unknown;
readonly code: string;
readonly httpStatus: number;
readonly path?: string;
}
data,
zodError: anyzodError: opts: V11ErrorFormatterOptsopts.V11ErrorFormatterOpts.error: AnyTrpcErrorerror.TrpcError<string, any>.code: stringcode === 'BAD_REQUEST' ? opts: V11ErrorFormatterOptsopts.V11ErrorFormatterOpts.error: AnyTrpcErrorerror.TrpcError<string, any>.data: anydata : null,
},
}),
});Outputs go out as v11 sent them, JSON.stringify after the transformer, so the old apps' types still describe them. They keep compiling against the v11 AppRouter type they were built with; nothing on the client changes.
What differs from a v11 server:
createContextruns once per WebSocket message, as for v12 sockets, not once per connection. It getsinfo.connectionParams.httpLinkandhttpBatchLinkget an output's promises settled; a procedure that returns an async iterable needshttpBatchStreamLink(as in v11).responseMetais gone: procedures set headers and status on theirresponsehandle, and the v11 handler sends them.