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

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: Procedure<{
    type: "query";
    ctx: Context;
    meta: {};
    input: {
        id: string;
    };
    output: Post | undefined;
    errors: SchemaValidationError<z.ZodObject<{
        id: z.ZodString;
    }, z.core.$strip>>;
    services: PostRepo;
}>
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>): Procedure<{
    type: "query";
    ctx: Context;
    meta: {};
    input: {
        id: string;
    };
    output: Post | undefined;
    errors: SchemaValidationError<z.ZodObject<{
        id: z.ZodString;
    }, z.core.$strip>>;
    services: PostRepo;
}>
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: Procedure<{
        type: "query";
        ctx: Context;
        meta: {};
        input: {
            id: string;
        };
        output: Post | undefined;
        errors: SchemaValidationError<z.ZodObject<{
            id: z.ZodString;
        }, z.core.$strip>>;
        services: PostRepo;
    }>;
}, undefined>(opts: FetchHandlerOptions<{
    byId: Procedure<{
        type: "query";
        ctx: Context;
        meta: {};
        input: {
            id: string;
        };
        output: Post | undefined;
        errors: SchemaValidationError<z.ZodObject<{
            id: z.ZodString;
        }, z.core.$strip>>;
        services: PostRepo;
    }>;
}, undefined>): FetchHandler<...>
createFetchHandler
({
BaseHandlerOptions<{ byId: Procedure<{ type: "query"; ctx: Context; meta: {}; input: { id: string; }; output: Post | undefined; errors: SchemaValidationError<ZodObject<{ id: ZodString; }, $strip>>; services: PostRepo; }>; }>.router: {
    byId: Procedure<{
        type: "query";
        ctx: Context;
        meta: {};
        input: {
            id: string;
        };
        output: Post | undefined;
        errors: SchemaValidationError<z.ZodObject<{
            id: z.ZodString;
        }, z.core.$strip>>;
        services: PostRepo;
    }>;
}
router
: {
byId: Procedure<{
    type: "query";
    ctx: Context;
    meta: {};
    input: {
        id: string;
    };
    output: Post | undefined;
    errors: SchemaValidationError<z.ZodObject<{
        id: z.ZodString;
    }, z.core.$strip>>;
    services: PostRepo;
}>
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 class PostNotFoundPostNotFound extends class TrpcError<TCode extends string = string, TData = unknown>TrpcError.
TrpcError<TCode extends string = string, TData = unknown>.Tagged<"POST_NOT_FOUND", Schema.Struct<{
    readonly id: Schema.String;
}>, 404>(code: "POST_NOT_FOUND", spec?: TaggedErrorSpec<Schema.Struct<{
    readonly id: Schema.String;
}>, 404>): TaggedErrorClass<"POST_NOT_FOUND", Schema.Struct<{
    readonly id: Schema.String;
}>, 404>
Tagged
('POST_NOT_FOUND', {
TaggedErrorSpec<Struct<{ readonly id: String; }>, 404>.status?: 404 | undefinedstatus: 404,
TaggedErrorSpec<Struct<{ readonly id: String; }>, 404>.data?: Schema.Struct<{
    readonly id: Schema.String;
}> | undefined
data
: import SchemaSchema.
function Struct<{
    readonly id: Schema.String;
}>(fields: {
    readonly id: Schema.String;
}): Schema.Struct<{
    readonly id: Schema.String;
}>
Struct
({ id: Schema.Stringid: import SchemaSchema.const String: Schema.StringString }),
}) {} 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, PostNotFound, never>>(resolver: (opts: ResolverOpts<InputDef<RootDef<Context, {}>, z.ZodObject<{
    id: z.ZodString;
}, z.core.$strip>>>) => Effect.Effect<Post, PostNotFound, never>): Procedure<{
    type: "query";
    ctx: Context;
    meta: {};
    input: {
        id: string;
    };
    output: Post;
    errors: PostNotFound | SchemaValidationError<z.ZodObject<{
        id: z.ZodString;
    }, z.core.$strip>>;
    services: never;
}>
query
((
opts: ResolverOpts<InputDef<RootDef<Context, {}>, z.ZodObject<{
    id: z.ZodString;
}, z.core.$strip>>>
opts
) =>
import EffectEffect.const gen: <Effect.Effect<never, PostNotFound, never> | Effect.Effect<Post | undefined, never, never>, Post>(f: () => Generator<Effect.Effect<never, PostNotFound, never> | Effect.Effect<Post | undefined, never, never>, Post, never>) => Effect.Effect<Post, PostNotFound, never> (+1 overload)gen(function* () { const const post: Post | undefinedpost = 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);
if (!const post: Post | undefinedpost) return yield* new
constructor PostNotFound(data: {
    readonly id: string;
}, opts?: TaggedErrorOptions | undefined): PostNotFound
PostNotFound
({ id: stringid:
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 });
return const post: Postpost; }), );

Middleware that provides services

t.middleware.effect<{ provides: S }>()(fn) writes a middleware whose next() is an Effect, so it can provide services:

const 
const withUser: EffectMiddleware<{
    user: User | null;
}, unknown, {}, {}, never, never, CurrentUser, never, Unset>
withUser
= const t: TrpcRoot<Context, {}, RootDef<Context, {}>>t.
TrpcRoot<Context, {}, RootDef<Context, {}>>.middleware: RootMiddleware<{
    user: User | null;
}, {}>
middleware
.
RootMiddleware<{ user: User | null; }, {}>.effect<{
    provides: CurrentUser;
}>(): EffectMiddlewareFactory<{
    user: User | null;
}, unknown, {}, CurrentUser>
effect
<{ provides: CurrentUserprovides: class CurrentUserCurrentUser }>()((
opts: EffectMiddlewareOpts<{
    user: User | null;
}, unknown, {}, CurrentUser>
opts
) =>
opts: EffectMiddlewareOpts<{
    user: User | null;
}, unknown, {}, CurrentUser>
opts
.
EffectMiddlewareOpts<{ user: User | null; }, unknown, {}, CurrentUser>.next: EffectNextFn
() => Effect.Effect<MiddlewareResult<{}, Unset>, never, CurrentUser> (+2 overloads)
next
().Pipeable.pipe<Effect.Effect<MiddlewareResult<{}, Unset>, never, CurrentUser>, Effect.Effect<MiddlewareResult<{}, Unset>, never, never>>(this: Effect.Effect<MiddlewareResult<{}, Unset>, never, CurrentUser>, ab: (_: Effect.Effect<MiddlewareResult<{}, Unset>, never, CurrentUser>) => Effect.Effect<MiddlewareResult<{}, Unset>, never, never>): Effect.Effect<...> (+21 overloads)pipe(import EffectEffect.const provideService: <CurrentUser, User | null>(service: Context.Key<CurrentUser, User | null>, implementation: User | null) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, CurrentUser>> (+2 overloads)provideService(class CurrentUserCurrentUser,
opts: EffectMiddlewareOpts<{
    user: User | null;
}, unknown, {}, CurrentUser>
opts
.
ctx: {
    user: User | null;
}
ctx
.user: User | nulluser)),
);

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 call continues the trace in its message's trace field, and starts its own trace without one.

examples/effect-observability puts these together: an Effect middleware that tags the call's span, service spans under it, and a mail sent with Effect.forkDetach whose span joins the trace after the response.

Work after the response

Effect.forkDetach starts a fiber that keeps running after the call returns, on a long-lived server:

const t: TrpcRoot<Context, {}, RootDef<Context, {}>>t.TrpcRoot<Context, {}, RootDef<Context, {}>>.procedure: ProcedureBuilder<RootDef<Context, {}>>procedure.
ProcedureBuilder<RootDef<Context, {}>>.mutation<Effect.Effect<User, never, never>>(resolver: (opts: ResolverOpts<RootDef<Context, {}>>) => Effect.Effect<User, never, never>): Procedure<{
    type: "mutation";
    ctx: Context;
    meta: {};
    input: undefined;
    output: User;
    errors: never;
    services: never;
}>
mutation
((opts: ResolverOpts<RootDef<Context, {}>>opts) =>
import EffectEffect.const gen: <Effect.Effect<User, never, never> | Effect.Effect<Fiber<void, never>, never, never>, User>(f: () => Generator<Effect.Effect<User, never, never> | Effect.Effect<Fiber<void, never>, never, never>, User, never>) => Effect.Effect<User, never, never> (+1 overload)gen(function* () { const const user: Useruser = yield*
const Users: {
    create: (input: unknown) => Effect.Effect<User>;
}
Users
.create: (input: unknown) => Effect.Effect<User>create(opts: ResolverOpts<RootDef<Context, {}>>opts.ResolverOpts<RootDef<Context, {}>>.input: undefinedinput);
yield* import EffectEffect.
const forkDetach: <Effect.Effect<void, never, never>>(effectOrOptions?: Effect.Effect<void, never, never> | undefined, options?: {
    readonly startImmediately?: boolean | undefined;
    readonly uninterruptible?: boolean | "inherit" | undefined;
} | undefined) => Effect.Effect<Fiber<void, never>, never, never>
forkDetach
(
const Mailer: {
    welcome: (user: User) => Effect.Effect<void>;
}
Mailer
.welcome: (user: User) => Effect.Effect<void>welcome(const user: Useruser));
return const user: Useruser; }), );

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