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

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);
user: 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);
ResolverOpts<With<InputDef<RootDef<Context, {}>, ZodObject<{ id: ZodString; }, $strip>>, { ctx: Context; ctxAdded: {}; inputOut: { ...; }; errors: AnyTrpcError | SchemaValidationError<...>; shorts: never; }>>.input: {
    post: Post | undefined;
}

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.