Skip to content
tRPC

Node

Serve on Node with Effect's HttpServer, or mount the handler in Express, Fastify and node:http.

On this page

The Effect way

import { function createServer<Request extends typeof IncomingMessage = typeof IncomingMessage, Response extends typeof ServerResponse = typeof ServerResponse>(requestListener?: RequestListener<Request, Response>): Server<Request, Response> (+1 overload)createServer } from 'node:http';
import { import NodeHttpServerNodeHttpServer, import NodeRuntimeNodeRuntime } from '@effect/platform-node';
import { import LayerLayer } from 'effect';
import { function serve(handler: FetchLike | (FetchLike & WebSocketHandlerLike), opts?: HttpOptions): Layer.Layer<never, never, HttpServer>serve } from '@trpc12/trpc/http';

function serve(handler: FetchLike | (FetchLike & WebSocketHandlerLike), opts?: HttpOptions): Layer.Layer<never, never, HttpServer>serve(const handler: FetchHandler<undefined>handler).Pipeable.pipe<Layer.Layer<never, never, HttpServer>, Layer.Layer<never, ServeError, never>, Effect<never, ServeError, never>, void>(this: Layer.Layer<never, never, HttpServer>, ab: (_: Layer.Layer<never, never, HttpServer>) => Layer.Layer<never, ServeError, never>, bc: (_: Layer.Layer<never, ServeError, never>) => Effect<never, ServeError, never>, cd: (_: Effect<never, ServeError, never>) => void): void (+21 overloads)pipe(
  import LayerLayer.const provide: <never, ServeError, HttpServer | NodeServices | HttpPlatform | Generator>(that: Layer.Layer<HttpServer | NodeServices | HttpPlatform | Generator, ServeError, never>) => <RIn2, E2, ROut2>(self: Layer.Layer<ROut2, E2, RIn2>) => Layer.Layer<ROut2, ServeError | E2, Exclude<RIn2, HttpServer | NodeServices | HttpPlatform | Generator>> (+3 overloads)provide(import NodeHttpServerNodeHttpServer.const layer: (evaluate: LazyArg<Server>, options: NodeHttpServer.Options) => Layer.Layer<HttpServer | NodeServices | HttpPlatform | Generator, ServeError>layer(function createServer<Request extends typeof IncomingMessage = typeof IncomingMessage, Response extends typeof ServerResponse = typeof ServerResponse>(requestListener?: RequestListener<Request, Response>): Server<Request, Response> (+1 overload)createServer, { ListenOptions.port?: number | undefinedport: 3000 })),
  import LayerLayer.const launch: <RIn, E, ROut>(self: Layer.Layer<ROut, E, RIn>) => Effect<never, E, RIn>launch,
  import NodeRuntimeNodeRuntime.
const runMain: {
    (options?: {
        readonly disableErrorReporting?: boolean | undefined;
        readonly teardown?: Teardown | undefined;
    }): <E, A>(effect: Effect<A, E>) => void;
    <E, A>(effect: Effect<A, E>, options?: {
        readonly disableErrorReporting?: boolean | undefined;
        readonly teardown?: Teardown | undefined;
    }): void;
}
runMain
,
);

serve takes endpoint, the path WebSocket upgrades must use. Any Effect HttpServer layer works in place of Node's.

Routes next to your own

The handler is an effect/http app underneath: createContext gets Effect's HttpServerRequest, and on Node the request is never converted to a web Request. handler.routes is that app as HttpRouter routes for the endpoint and the paths under it, WebSocket upgrades included, and serve(handler) serves exactly those, so other paths get a 404. Merge them with your own routes:

import { import HttpRouterHttpRouter, import HttpServerResponseHttpServerResponse } from 'effect/http';

const const app: Layer.Layer<never, never, HttpRouter.HttpRouter>app = import LayerLayer.const mergeAll: <[Layer.Layer<never, never, HttpRouter.HttpRouter>, Layer.Layer<never, never, HttpRouter.HttpRouter>]>(layers_0: Layer.Layer<never, never, HttpRouter.HttpRouter>, layers_1: Layer.Layer<never, never, HttpRouter.HttpRouter>) => Layer.Layer<never, never, HttpRouter.HttpRouter>mergeAll(
  const handler: FetchHandler<undefined>handler.FetchHandler<undefined>.routes: Layer.Layer<never, never, HttpRouter.HttpRouter>routes,
  import HttpRouterHttpRouter.
const add: <never, never>(method: "*" | "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "OPTIONS" | "QUERY", path: HttpRouter.PathInput, handler: HttpServerResponse.HttpServerResponse | Effect<HttpServerResponse.HttpServerResponse, never, never> | ((request: HttpServerRequest) => Effect<HttpServerResponse.HttpServerResponse, never, never>), options?: {
    readonly uninterruptible?: boolean | undefined;
}) => Layer.Layer<...>
add
('GET', '/health', import HttpServerResponseHttpServerResponse.const text: (body: string, options?: HttpServerResponse.Options.WithContentType) => HttpServerResponse.HttpServerResponsetext('ok')),
); import HttpRouterHttpRouter.
const serve: <never, never, HttpRouter.HttpRouter, unknown, never>(appLayer: Layer.Layer<never, never, HttpRouter.HttpRouter>, options?: {
    readonly routerConfig?: Partial<RouterConfig> | undefined;
    readonly disableLogger?: boolean | undefined;
    readonly disableListenLog?: boolean;
    readonly middleware?: ((effect: Effect<HttpServerResponse.HttpServerResponse, HttpServerError, HttpServerRequest | Scope>) => Effect<HttpServerResponse.HttpServerResponse, unknown, never>) | undefined;
} | undefined) => Layer.Layer<...>
serve
(const app: Layer.Layer<never, never, HttpRouter.HttpRouter>app).Pipeable.pipe<Layer.Layer<never, never, HttpServer>, Layer.Layer<never, ServeError, never>, Effect<never, ServeError, never>, void>(this: Layer.Layer<never, never, HttpServer>, ab: (_: Layer.Layer<never, never, HttpServer>) => Layer.Layer<never, ServeError, never>, bc: (_: Layer.Layer<never, ServeError, never>) => Effect<never, ServeError, never>, cd: (_: Effect<never, ServeError, never>) => void): void (+21 overloads)pipe(
import LayerLayer.const provide: <never, ServeError, HttpServer | NodeServices | HttpPlatform | Generator>(that: Layer.Layer<HttpServer | NodeServices | HttpPlatform | Generator, ServeError, never>) => <RIn2, E2, ROut2>(self: Layer.Layer<ROut2, E2, RIn2>) => Layer.Layer<ROut2, ServeError | E2, Exclude<RIn2, HttpServer | NodeServices | HttpPlatform | Generator>> (+3 overloads)provide(import NodeHttpServerNodeHttpServer.const layer: (evaluate: LazyArg<Server>, options: NodeHttpServer.Options) => Layer.Layer<HttpServer | NodeServices | HttpPlatform | Generator, ServeError>layer(function createServer<Request extends typeof IncomingMessage = typeof IncomingMessage, Response extends typeof ServerResponse = typeof ServerResponse>(requestListener?: RequestListener<Request, Response>): Server<Request, Response> (+1 overload)createServer, { ListenOptions.port?: number | undefinedport: 3000 })), import LayerLayer.const launch: <RIn, E, ROut>(self: Layer.Layer<ROut, E, RIn>) => Effect<never, E, RIn>launch, import NodeRuntimeNodeRuntime.
const runMain: {
    (options?: {
        readonly disableErrorReporting?: boolean | undefined;
        readonly teardown?: Teardown | undefined;
    }): <E, A>(effect: Effect<A, E>) => void;
    <E, A>(effect: Effect<A, E>, options?: {
        readonly disableErrorReporting?: boolean | undefined;
        readonly teardown?: Teardown | undefined;
    }): void;
}
runMain
,
);

handler.fetch runs the same app through HttpRouter.toWebHandler. To use the handler as one app instead of routes, toHttpEffect(handler) is the app itself.

Express, Fastify and node:http

NodeHttpServer.makeHandler turns the Effect app into a (req, res) handler. Its resources live in the scope you pass.

import { import NodeHttpServerNodeHttpServer } from '@effect/platform-node';
import { import EffectEffect, import ExitExit, import ScopeScope } from 'effect';
import { function toHttpEffect(handler: FetchLike | (FetchLike & WebSocketHandlerLike), opts?: HttpOptions): Effect.Effect<HttpServerResponse, HttpServerError, HttpServerRequest>toHttpEffect } from '@trpc12/trpc/http';

const const scope: Scope.Closeablescope = import ScopeScope.const makeUnsafe: (finalizerStrategy?: "sequential" | "parallel") => Scope.CloseablemakeUnsafe();
const const nodeHandler: (nodeRequest: IncomingMessage, nodeResponse: ServerResponse) => voidnodeHandler = await import EffectEffect.const runPromise: <(nodeRequest: IncomingMessage, nodeResponse: ServerResponse) => void, never>(effect: Effect.Effect<(nodeRequest: IncomingMessage, nodeResponse: ServerResponse) => void, never, never>, options?: Effect.RunOptions | undefined) => Promise<(nodeRequest: IncomingMessage, nodeResponse: ServerResponse) => void>runPromise(
  import NodeHttpServerNodeHttpServer.
const makeHandler: <HttpServerRequest, HttpServerError, Effect.Effect<HttpServerResponse, HttpServerError, HttpServerRequest>>(httpEffect: Effect.Effect<HttpServerResponse, HttpServerError, HttpServerRequest>, options: {
    readonly scope: Scope.Scope;
    readonly middleware?: HttpMiddleware.Applied<Effect.Effect<HttpServerResponse, HttpServerError, HttpServerRequest>, HttpServerError, HttpServerRequest> | undefined;
}) => Effect.Effect<...>
makeHandler
(function toHttpEffect(handler: FetchLike | (FetchLike & WebSocketHandlerLike), opts?: HttpOptions): Effect.Effect<HttpServerResponse, HttpServerError, HttpServerRequest>toHttpEffect(const handler: FetchHandler<undefined>handler), { scope: Scope.Scopescope }),
); // on shutdown: await Effect.runPromise(Scope.close(scope, Exit.void))
// Express 5: a route, not app.use('/trpc', …), which strips the prefix from req.url
app.all('/trpc/{*path}', nodeHandler);

// Fastify: hand over the raw request, unparsed
fastify.register(async (scoped) => {
  scoped.removeAllContentTypeParsers();
  scoped.addContentTypeParser('*', (_req, _payload, done) => done(null));
  scoped.all('/trpc/*', (req, reply) => {
    reply.hijack();
    nodeHandler(req.raw, reply.raw);
  });
});

// node:http
createServer((req, res) => nodeHandler(req, res)).listen(3000);

The handler reads the body itself. If express.json(), express.text() or express.raw() ran first, it uses req.body. Other parsers that consume the stream (for example multipart ones) make the call fail with a 500 that says so, so mount them after the tRPC route.

Middleware and CORS apply the same way on every adapter.