Skip to content
tRPC

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).