Server
Effect
Resolvers that return Effects, services from the handler's layer, typed failures and tracing.
On this page
tRPC v12 is built on Effect, and using it is optional: resolvers can return plain values and promises. A resolver can also return an Effect or a Stream. Its failures become errors, and the services it needs become the handler's required layer:
import { function createFetchHandler<TRouter extends AnyRouter, TPlatform = undefined>(opts: FetchHandlerOptions<TRouter, TPlatform>): FetchHandler<TPlatform>createFetchHandler } from '@trpc12/trpc/server';
import { import ContextContext, import EffectEffect } from 'effect';
import { import zz } from 'zod';
class class PostRepoPostRepo extends import ContextContext.const Service: <PostRepo, {
find: (id: string) => Effect.Effect<Post | undefined>;
}>() => <Identifier, E, R, Args>(id: Identifier, options?: {
readonly make?: ((...args: Args) => Effect.Effect<{
find: (id: string) => Effect.Effect<Post | undefined>;
}, E, R>) | Effect.Effect<{
find: (id: string) => Effect.Effect<Post | undefined>;
}, E, R> | undefined;
} | undefined) => Context.ServiceClass<PostRepo, Identifier, {
find: (id: string) => Effect.Effect<Post | undefined>;
}> & ([...] extends [...] ? unknown : {
...;
}) (+2 overloads)
Service<
class PostRepoPostRepo,
{ find: (id: string) => Effect.Effect<Post | undefined>find: (id: stringid: string) => import EffectEffect.interface Effect<out A, out E = never, out R = never>Effect<Post | undefined> }
>()('PostRepo') {}
const const byId: BuildProcedure<InputDef<RootDef<Context, {}>, z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>, "query", {
output: Post | undefined;
errors: never;
services: PostRepo;
unmapped: never;
}>
byId = const t: TrpcRoot<Context, {}, RootDef<Context, {}>>t.TrpcRoot<Context, {}, RootDef<Context, {}>>.procedure: ProcedureBuilder<RootDef<Context, {}>>procedure.ProcedureBuilder<RootDef<Context, {}>>.input<z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>(schema: z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>): ProcedureBuilder<InputDef<RootDef<Context, {}>, z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>> (+1 overload)
input(import zz.function object<{
id: z.ZodString;
}>(shape?: {
id: z.ZodString;
} | undefined, params?: string | {
error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueInvalidType<unknown> | z.core.$ZodIssueUnrecognizedKeys>> | undefined;
message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>
object({ id: z.ZodStringid: import zz.function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)string() })).ProcedureBuilder<InputDef<RootDef<Context, {}>, ZodObject<{ id: ZodString; }, $strip>>>.query<Effect.Effect<Post | undefined, never, PostRepo>>(resolver: (opts: ResolverOpts<InputDef<RootDef<Context, {}>, z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>>) => Effect.Effect<Post | undefined, never, PostRepo>): BuildProcedure<InputDef<RootDef<Context, {}>, z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>, "query", {
output: Post | undefined;
errors: never;
services: PostRepo;
unmapped: never;
}>
query((opts: ResolverOpts<InputDef<RootDef<Context, {}>, z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>>
opts) =>
import EffectEffect.const gen: <Effect.Effect<Post | undefined, never, never> | Effect.Effect<{
find: (id: string) => Effect.Effect<Post | undefined>;
}, never, PostRepo>, Post | undefined>(f: () => Generator<Effect.Effect<Post | undefined, never, never> | Effect.Effect<{
find: (id: string) => Effect.Effect<Post | undefined>;
}, never, PostRepo>, Post | undefined, never>) => Effect.Effect<Post | undefined, never, PostRepo> (+1 overload)
gen(function* () {
const const repo: {
find: (id: string) => Effect.Effect<Post | undefined>;
}
repo = yield* class PostRepoPostRepo;
return yield* const repo: {
find: (id: string) => Effect.Effect<Post | undefined>;
}
repo.find: (id: string) => Effect.Effect<Post | undefined>find(opts: ResolverOpts<InputDef<RootDef<Context, {}>, z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>>
opts.ResolverOpts<InputDef<RootDef<Context, {}>, ZodObject<{ id: ZodString; }, $strip>>>.input: {
id: string;
}
input.id: stringid);
}),
);
createFetchHandler<{
byId: BuildProcedure<InputDef<RootDef<Context, {}>, z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>, "query", {
output: Post | undefined;
errors: never;
services: PostRepo;
unmapped: never;
}>;
}, undefined>(opts: FetchHandlerOptions<{
byId: BuildProcedure<InputDef<RootDef<Context, {}>, z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>, "query", {
output: Post | undefined;
errors: never;
services: PostRepo;
unmapped: never;
}>;
}, undefined>): FetchHandler<...>
createFetchHandler({
BaseHandlerOptions<{ byId: BuildProcedure<InputDef<RootDef<Context, {}>, ZodObject<{ id: ZodString; }, $strip>>, "query", { output: Post | undefined; errors: never; services: PostRepo; unmapped: never; }>; }>.router: {
byId: BuildProcedure<InputDef<RootDef<Context, {}>, z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>, "query", {
output: Post | undefined;
errors: never;
services: PostRepo;
unmapped: never;
}>;
}
router: { byId: BuildProcedure<InputDef<RootDef<Context, {}>, z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>, "query", {
output: Post | undefined;
errors: never;
services: PostRepo;
unmapped: never;
}>
byId },
createContext: (opts: CreateContextOpts<undefined>) => MaybePromise<{
user: User | null;
}>
createContext: () => ({ user: User | nulluser: null }),
layer: Layer<PostRepo, any, never>layer: const PostRepoLive: Layer<PostRepo, never, never>PostRepoLive,
});Errors clients may see
A failure that isn't a TrpcError is a type error until it is mapped (Effect.catchTag), so internals like a database error's query never reach clients. Mark the errors clients may see by making them TrpcError.Tagged classes; yielding or throwing one sends it typed, with its data validated first:
class PostNotFound extends TrpcError.Tagged('POST_NOT_FOUND', {
status: 404,
data: Schema.Struct({ id: Schema.String }),
}) {}
t.procedure.query(({ input }) =>
Effect.gen(function* () {
const post = yield* repo.find(input.id);
if (!post) return yield* new PostNotFound({ id: input.id });
return post;
}),
);.errors({ POST_NOT_FOUND: PostNotFound }) documents one in OpenAPI.
Middleware that provides services
t.middleware.effect<{ provides: S }>()(fn) writes a middleware whose next() is an Effect, so it can provide services:
const withUser = t.middleware.effect<{ provides: CurrentUser }>()(
({ ctx, next }) => next().pipe(Effect.provideService(CurrentUser, ctx.user)),
);Tracing
Tracing is opt-in: put a tracer in the handler's layer (@effect/opentelemetry) and every call runs in an Effect span named trpc.<path>, with rpc.system, rpc.method and trpc.type attributes, plus trpc.error.code when it fails. Spans opened in middlewares and resolvers nest under it. Without a tracer, calls open no span, so requests don't pay for one.
The span continues the request's trace: the http.server span on Effect HTTP servers, or the incoming traceparent header on handler.fetch, so a traced client and the server share one trace (Client tracing). A WebSocket or MessagePort call continues the trace in its message's trace field, and starts its own trace without one.
Work after the response
Effect.forkDetach starts a fiber that keeps running after the call returns, on a long-lived server:
t.procedure.mutation(({ input }) =>
Effect.gen(function* () {
const user = yield* Users.create(input);
yield* Effect.forkDetach(Mailer.welcome(user));
return user;
}),
);Serverless runtimes may freeze the instance once the response is sent. There, hand the promise to the platform: ctx.waitUntil() on Workers (see Fetch runtimes), after() in Next.js, waitUntil from @vercel/functions on Vercel.
The Effect client
On the client side, createEffectClient returns an Effect per call and a Stream per subscription (The Effect client).