Middleware
Base procedures that add to the context, replace the input or short-circuit a call.
On this page
Middleware runs before a procedure's resolver. .use() adds one to a procedure builder, and the builder becomes a base procedure to share:
import { function error<const TCode extends string, TData = undefined>(opts: Omit<TrpcErrorOptions<TCode, TData>, "defined">): TrpcError<TCode, TData>error } from '@trpc12/trpc/server';
const const authed: ProcedureBuilder<With<RootDef<Context, {}>, {
ctx: Context;
ctxAdded: {
user: User;
};
inputOut: Unset;
errors: TrpcError<"UNAUTHORIZED", undefined>;
shorts: never;
}>>
authed = const t: TrpcRoot<Context, {}, RootDef<Context, {}>>t.TrpcRoot<Context, {}, RootDef<Context, {}>>.procedure: ProcedureBuilder<RootDef<Context, {}>>procedure.ProcedureBuilder<RootDef<Context, {}>>.use<{
user: User;
}, TrpcError<"UNAUTHORIZED", undefined>, never, Unset, {}>(fn: MiddlewareFunction<{
user: User | null;
}, undefined, {}, {
user: User;
}, TrpcError<"UNAUTHORIZED", undefined>, never, Unset>): ProcedureBuilder<With<RootDef<Context, {}>, {
ctx: Context;
ctxAdded: {
user: User;
};
inputOut: Unset;
errors: TrpcError<"UNAUTHORIZED", undefined>;
shorts: never;
}>> (+1 overload)
use((opts: MiddlewareOpts<{
user: User | null;
}, undefined, {}>
opts) =>
opts: MiddlewareOpts<{
user: User | null;
}, undefined, {}>
opts.MiddlewareOpts<{ user: User | null; }, undefined, {}>.ctx: {
user: User | null;
}
ctx.user: User | nulluser
? opts: MiddlewareOpts<{
user: User | null;
}, undefined, {}>
opts.MiddlewareOpts<{ user: User | null; }, undefined, {}>.next: NextFn
<{
user: User;
}>(opts: {
ctx: {
user: User;
};
}) => Promise<MiddlewareResult<{
user: User;
}, Unset>> (+2 overloads)
next({ ctx: {
user: User;
}
ctx: { user: Useruser: opts: MiddlewareOpts<{
user: User | null;
}, undefined, {}>
opts.MiddlewareOpts<{ user: User | null; }, undefined, {}>.ctx: {
user: User | null;
}
ctx.user: Useruser } })
: error<"UNAUTHORIZED", undefined>(opts: Omit<TrpcErrorOptions<"UNAUTHORIZED", undefined>, "defined">): TrpcError<"UNAUTHORIZED", undefined>error({ code: "UNAUTHORIZED"code: 'UNAUTHORIZED' }),
);
const const me: Procedure<{
type: "query";
ctx: Context;
meta: {};
input: undefined;
output: User;
errors: TrpcError<"UNAUTHORIZED", undefined>;
services: never;
}>
me = const authed: ProcedureBuilder<With<RootDef<Context, {}>, {
ctx: Context;
ctxAdded: {
user: User;
};
inputOut: Unset;
errors: TrpcError<"UNAUTHORIZED", undefined>;
shorts: never;
}>>
authed.ProcedureBuilder<With<RootDef<Context, {}>, { ctx: Context; ctxAdded: { user: User; }; inputOut: Unset; errors: TrpcError<"UNAUTHORIZED", undefined>; shorts: never; }>>.query<User>(resolver: (opts: ResolverOpts<With<RootDef<Context, {}>, {
ctx: Context;
ctxAdded: {
user: User;
};
inputOut: Unset;
errors: TrpcError<"UNAUTHORIZED", undefined>;
shorts: never;
}>>) => User): Procedure<{
type: "query";
ctx: Context;
meta: {};
input: undefined;
output: User;
errors: TrpcError<"UNAUTHORIZED", undefined>;
services: never;
}>
query((opts: ResolverOpts<With<RootDef<Context, {}>, {
ctx: Context;
ctxAdded: {
user: User;
};
inputOut: Unset;
errors: TrpcError<"UNAUTHORIZED", undefined>;
shorts: never;
}>>
opts) => opts: ResolverOpts<With<RootDef<Context, {}>, {
ctx: Context;
ctxAdded: {
user: User;
};
inputOut: Unset;
errors: TrpcError<"UNAUTHORIZED", undefined>;
shorts: never;
}>>
opts.ResolverOpts<With<RootDef<Context, {}>, { ctx: Context; ctxAdded: { user: User; }; inputOut: Unset; errors: TrpcError<"UNAUTHORIZED", undefined>; shorts: never; }>>.ctx: {
user: User;
}
ctx.user);next({ ctx }) adds to the ctx; ok(value) short-circuits with a result. An error() returned from a middleware joins the procedure's typed errors (Errors).
Middleware is per procedure: build base procedures and share them. Routers take none. t.middleware(fn) types a middleware once for reuse.
Replacing the input
next({ input }) replaces the input for the steps after it, and the resolver's input takes its type:
import { import zz } from 'zod';
const const byId: Procedure<{
type: "query";
ctx: Context;
meta: {};
input: {
id: string;
};
output: string | undefined;
errors: AnyTrpcError | SchemaValidationError<z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>;
services: 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>>>.use<object, AnyTrpcError, never, {
post: Post | undefined;
}, {}>(fn: MiddlewareFunction<{
user: User | null;
}, {
id: string;
}, {}, object, AnyTrpcError, never, {
post: Post | undefined;
}>): ProcedureBuilder<With<InputDef<RootDef<Context, {}>, z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>, {
ctx: Context;
ctxAdded: {};
inputOut: {
post: Post | undefined;
};
errors: AnyTrpcError | SchemaValidationError<z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>;
shorts: never;
}>> (+1 overload)
use(async (opts: MiddlewareOpts<{
user: User | null;
}, {
id: string;
}, {}>
opts) =>
opts: MiddlewareOpts<{
user: User | null;
}, {
id: string;
}, {}>
opts.MiddlewareOpts<{ user: User | null; }, { id: string; }, {}>.next: NextFn
<{
post: Post | undefined;
}, object>(opts: {
ctx?: object | undefined;
input: {
post: Post | undefined;
};
}) => Promise<MiddlewareResult<object, {
post: Post | undefined;
}>> (+2 overloads)
next({ input: {
post: Post | undefined;
}
input: { post: Post | undefinedpost: const db: {
post: {
find: (id: string) => Post | undefined;
list: () => Post[];
create: (input: {
title: string;
author?: string | null;
}) => Post;
};
}
db.post: {
find: (id: string) => Post | undefined;
list: () => Post[];
create: (input: {
title: string;
author?: string | null;
}) => Post;
}
post.find: (id: string) => Post | undefinedfind(opts: MiddlewareOpts<{
user: User | null;
}, {
id: string;
}, {}>
opts.MiddlewareOpts<{ user: User | null; }, { id: string; }, {}>.input: {
id: string;
}
input.id: stringid) } }),
)
.ProcedureBuilder<With<InputDef<RootDef<Context, {}>, ZodObject<{ id: ZodString; }, $strip>>, { ctx: Context; ctxAdded: {}; inputOut: { ...; }; errors: AnyTrpcError | SchemaValidationError<...>; shorts: never; }>>.query<string | undefined>(resolver: (opts: ResolverOpts<With<InputDef<RootDef<Context, {}>, z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>, {
ctx: Context;
ctxAdded: {};
inputOut: {
post: Post | undefined;
};
errors: AnyTrpcError | SchemaValidationError<z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>;
shorts: never;
}>>) => string | undefined): Procedure<{
type: "query";
ctx: Context;
meta: {};
input: {
id: string;
};
output: string | undefined;
errors: AnyTrpcError | SchemaValidationError<...>;
services: never;
}>
query((opts: ResolverOpts<With<InputDef<RootDef<Context, {}>, z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>, {
ctx: Context;
ctxAdded: {};
inputOut: {
post: Post | undefined;
};
errors: AnyTrpcError | SchemaValidationError<z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>;
shorts: never;
}>>
opts) => opts: ResolverOpts<With<InputDef<RootDef<Context, {}>, z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>, {
ctx: Context;
ctxAdded: {};
inputOut: {
post: Post | undefined;
};
errors: AnyTrpcError | SchemaValidationError<z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>;
shorts: never;
}>>
opts.input.post: Post | undefinedpost?.Post.title: string | undefinedtitle);The client still sends the schema's input. A later .input() parses the raw call input again, merged over the replaced one when both are objects.
Middleware for packages
middleware<{ ctx }>()(fn) (and middleware.effect) declares one outside any t, typed by the ctx it needs, for a package to publish. Used on a procedure that lacks a key it needs, the key becomes a requirement of the procedure's ctx, the way an Effect requirement moves to whoever runs it: the handler's createContext (or an in-process caller's ctx) must supply it. A key the procedure has with another type is a type error.
const const withTenant: MiddlewareFunction<{
tenant: string;
}, unknown, {}, {
db: {
posts(): Post[];
};
}, AnyTrpcError, never, Unset>
withTenant = middleware<{
ctx: {
tenant: string;
};
}>(): <$Ctx, $Err, $Ok, $Input>(fn: MiddlewareFunction<{
tenant: string;
}, unknown, {}, $Ctx, $Err, $Ok, $Input>) => MiddlewareFunction<{
tenant: string;
}, unknown, {}, $Ctx, $Err, $Ok, $Input>
middleware<{ ctx: {
tenant: string;
}
ctx: { tenant: stringtenant: string } }>()((opts: MiddlewareOpts<{
tenant: string;
}, unknown, {}>
opts) =>
opts: MiddlewareOpts<{
tenant: string;
}, unknown, {}>
opts.MiddlewareOpts<{ tenant: string; }, unknown, {}>.next: NextFn
<{
db: {
posts(): Post[];
};
}>(opts: {
ctx: {
db: {
posts(): Post[];
};
};
}) => Promise<MiddlewareResult<{
db: {
posts(): Post[];
};
}, Unset>> (+2 overloads)
next({ ctx: {
db: {
posts(): Post[];
};
}
ctx: { db: {
posts(): Post[];
}
db: function dbFor(tenant: string): {
posts(): Post[];
}
dbFor(opts: MiddlewareOpts<{
tenant: string;
}, unknown, {}>
opts.MiddlewareOpts<{ tenant: string; }, unknown, {}>.ctx: {
tenant: string;
}
ctx.tenant: stringtenant) } }),
);
const const router: {
posts: Procedure<{
type: "query";
ctx: {
tenant: string;
};
meta: {};
input: undefined;
output: Post[];
errors: AnyTrpcError;
services: never;
}>;
}
router = {
posts: Procedure<{
type: "query";
ctx: {
tenant: string;
};
meta: {};
input: undefined;
output: Post[];
errors: AnyTrpcError;
services: never;
}>
posts: const t: TrpcRoot<{}, {}, RootDef<{}, {}>>t.TrpcRoot<{}, {}, RootDef<{}, {}>>.procedure: ProcedureBuilder<RootDef<{}, {}>>procedure.ProcedureBuilder<RootDef<{}, {}>>.use<{
db: {
posts(): Post[];
};
}, AnyTrpcError, never, Unset, {
tenant: string;
}>(fn: MiddlewareFunction<{} & {
tenant: string;
}, undefined, {}, {
db: {
posts(): Post[];
};
}, AnyTrpcError, never, Unset>): ProcedureBuilder<With<RootDef<{}, {}>, {
ctx: {
tenant: string;
};
ctxAdded: {
db: {
posts(): Post[];
};
};
inputOut: Unset;
errors: AnyTrpcError;
shorts: never;
}>> (+1 overload)
use(const withTenant: MiddlewareFunction<{
tenant: string;
}, unknown, {}, {
db: {
posts(): Post[];
};
}, AnyTrpcError, never, Unset>
withTenant).ProcedureBuilder<With<RootDef<{}, {}>, { ctx: { tenant: string; }; ctxAdded: { db: { posts(): Post[]; }; }; inputOut: Unset; errors: AnyTrpcError; shorts: never; }>>.query<Post[]>(resolver: (opts: ResolverOpts<With<RootDef<{}, {}>, {
ctx: {
tenant: string;
};
ctxAdded: {
db: {
posts(): Post[];
};
};
inputOut: Unset;
errors: AnyTrpcError;
shorts: never;
}>>) => Post[]): Procedure<{
type: "query";
ctx: {
tenant: string;
};
meta: {};
input: undefined;
output: Post[];
errors: AnyTrpcError;
services: never;
}>
query((opts: ResolverOpts<With<RootDef<{}, {}>, {
ctx: {
tenant: string;
};
ctxAdded: {
db: {
posts(): Post[];
};
};
inputOut: Unset;
errors: AnyTrpcError;
shorts: never;
}>>
opts) => opts: ResolverOpts<With<RootDef<{}, {}>, {
ctx: {
tenant: string;
};
ctxAdded: {
db: {
posts(): Post[];
};
};
inputOut: Unset;
errors: AnyTrpcError;
shorts: never;
}>>
opts.ResolverOpts<With<RootDef<{}, {}>, { ctx: { tenant: string; }; ctxAdded: { db: { posts(): Post[]; }; }; inputOut: Unset; errors: AnyTrpcError; shorts: never; }>>.ctx: {
tenant: string;
db: {
posts(): Post[];
};
}
ctx.db: {
posts(): Post[];
}
db.function posts(): Post[]posts()),
};
createFetchHandler<{
posts: Procedure<{
type: "query";
ctx: {
tenant: string;
};
meta: {};
input: undefined;
output: Post[];
errors: AnyTrpcError;
services: never;
}>;
}, undefined>(opts: FetchHandlerOptions<{
posts: Procedure<{
type: "query";
ctx: {
tenant: string;
};
meta: {};
input: undefined;
output: Post[];
errors: AnyTrpcError;
services: never;
}>;
}, undefined>): FetchHandler<undefined>
createFetchHandler({
BaseHandlerOptions<{ posts: Procedure<{ type: "query"; ctx: { tenant: string; }; meta: {}; input: undefined; output: Post[]; errors: AnyTrpcError; services: never; }>; }>.router: {
posts: Procedure<{
type: "query";
ctx: {
tenant: string;
};
meta: {};
input: undefined;
output: Post[];
errors: AnyTrpcError;
services: never;
}>;
}
router,
createContext: (opts: CreateContextOpts<undefined>) => MaybePromise<{
tenant: string;
}>
createContext: (opts: CreateContextOpts<undefined>opts) => ({ tenant: stringtenant: function tenantOf(request: HttpServerRequest.HttpServerRequest): stringtenantOf(opts: CreateContextOpts<undefined>opts.CreateContextOpts<undefined>.request: HttpServerRequest.HttpServerRequestrequest) }),
});.concat(plugin) stays strict: a plugin builder's ctx must already be there.
Running once
When the plugin starts with the same middleware as the builder (the same function, in the same order), .concat() runs it once, on a builder or on t. Elsewhere, middleware applied twice runs twice. For work that should happen once per request, an Effect service in the handler's layer is built once, and a middleware can reuse what ctx already has, so nested call(other, input, { ctx })s skip it:
const const withDb: MiddlewareFunction<{
db?: Db;
}, unknown, {}, {
db: Db;
}, AnyTrpcError, never, Unset>
withDb = middleware<{
ctx: {
db?: Db;
};
}>(): <$Ctx, $Err, $Ok, $Input>(fn: MiddlewareFunction<{
db?: Db;
}, unknown, {}, $Ctx, $Err, $Ok, $Input>) => MiddlewareFunction<{
db?: Db;
}, unknown, {}, $Ctx, $Err, $Ok, $Input>
middleware<{ ctx: {
db?: Db;
}
ctx: { db?: Db | undefineddb?: Db } }>()(async (opts: MiddlewareOpts<{
db?: Db;
}, unknown, {}>
opts) =>
opts: MiddlewareOpts<{
db?: Db;
}, unknown, {}>
opts.MiddlewareOpts<{ db?: Db; }, unknown, {}>.next: NextFn
<{
db: Db;
}>(opts: {
ctx: {
db: Db;
};
}) => Promise<MiddlewareResult<{
db: Db;
}, Unset>> (+2 overloads)
next({ ctx: {
db: Db;
}
ctx: { db: Dbdb: opts: MiddlewareOpts<{
db?: Db;
}, unknown, {}>
opts.MiddlewareOpts<{ db?: Db; }, unknown, {}>.ctx: {
db?: Db;
}
ctx.db?: Db | undefineddb ?? (await function connect(): Promise<Db>connect()) } }),
);Effect middleware
t.middleware.effect<{ provides: S }>()(fn) writes a middleware whose next() is an Effect, so it can provide services (Effect).
Built-in middleware
@trpc12/trpc/middleware has ordinary middleware for common jobs, such as lifecycle hooks (Built-in middleware).
To add middleware, ctx, meta and errors to every procedure of an app from a package, see Extensions.