Skip to content

tRPC v12 isn’t on npm yet. The @trpc12/* packages on this site can’t be installed until it’s released. Use v11 today

tRPC

Adapters

One handler serves HTTP, SSE and WebSockets on every runtime; pick the adapter for yours.

On this page

createFetchHandler() returns a web-standard fetch(request). Anything that speaks Request and Response can serve it directly. On Effect's HttpServer, @trpc12/trpc/http adds WebSockets, client-disconnect aborts, and Effect's platform layers.

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
,
createContext: (opts: CreateContextOpts<undefined>) => MaybePromise<{
    user: User | null;
}>
createContext
, layer?: Layer.Layer<never, any, never> | undefinedlayer });
// handler.fetch(request): Promise<Response> // handler.websocket(socket, { request }) // handler.dispose(): releases the layer

Procedures are served under endpoint, /trpc by default.

RuntimeServe withWebSockets
Nodeserve(handler) on NodeHttpServer, or Expressyes
BunBun.serve({ fetch: handler.fetch })with serve(handler)
DenoDeno.servehandler.websocket
Workers, Next.jshandler.fetchin a Durable Object
AWS LambdatoLambdaHandler(handler)no

Middleware and CORS

Request-level concerns are Effect HttpMiddleware in the handler's middleware option, outermost first. They wrap handler.fetch and, under serve()/toHttpEffect, WebSocket upgrades too. Call-level concerns are root t.use() middleware.

import { import HttpMiddlewareHttpMiddleware } from 'effect/http';
import { function cors(options?: CorsOptions): HttpMiddleware.HttpMiddlewarecors } from '@trpc12/trpc/http';

const const handler: FetchHandler<undefined>handler = 
createFetchHandler<{
    health: Procedure<{
        type: "query";
        ctx: {};
        meta: {};
        input: undefined;
        output: string;
        errors: never;
        services: never;
    }>;
}, undefined>(opts: FetchHandlerOptions<{
    health: Procedure<{
        type: "query";
        ctx: {};
        meta: {};
        input: undefined;
        output: string;
        errors: never;
        services: never;
    }>;
}, undefined>): FetchHandler<undefined>
createFetchHandler
({
BaseHandlerOptions<{ health: Procedure<{ type: "query"; ctx: {}; meta: {}; input: undefined; output: string; errors: never; services: never; }>; }>.router: {
    health: Procedure<{
        type: "query";
        ctx: {};
        meta: {};
        input: undefined;
        output: string;
        errors: never;
        services: never;
    }>;
}
router
,
BaseHandlerOptions<TRouter extends AnyRouter>.middleware?: readonly HttpMiddleware.HttpMiddleware[] | undefinedmiddleware: [ function cors(options?: CorsOptions): HttpMiddleware.HttpMiddlewarecors({ allowedOrigins?: readonly string[] | Predicate<string> | undefinedallowedOrigins: ['https://app.example.com'], credentials?: boolean | undefinedcredentials: true }), import HttpMiddlewareHttpMiddleware.const logger: <E, R>(httpApp: Effect<HttpServerResponse, E, HttpServerRequest | R>) => Effect<HttpServerResponse, E, HttpServerRequest | R>logger, ], });

cors() is Effect's HttpMiddleware.cors plus an origin check on WebSocket upgrades (403 for other origins). Browsers send cookies with cross-site upgrades and CORS doesn't cover them, so use it when you authenticate with cookies.

Compression

@trpc12/trpc doesn't compress. Your CDN or proxy (Cloudflare, Vercel, nginx) already negotiates Accept-Encoding, and does it better. If you add a compressing HttpMiddleware, skip application/jsonl, application/vnd.trpc.frames and text/event-stream responses: a gzip stream buffers until its window fills, which stalls subscriptions and streamed results.