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 layerProcedures are served under endpoint, /trpc by default.
| Runtime | Serve with | WebSockets |
|---|---|---|
| Node | serve(handler) on NodeHttpServer, or Express | yes |
| Bun | Bun.serve({ fetch: handler.fetch }) | with serve(handler) |
| Deno | Deno.serve | handler.websocket |
| Workers, Next.js | handler.fetch | in a Durable Object |
| AWS Lambda | toLambdaHandler(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.