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