Skip to content
tRPC

Extensions

Packages that add middleware, ctx, meta keys, errors and services to every procedure with t.concat().

On this page

A package can add middleware, ctx, meta keys, declared errors and Effect services to every procedure of an app. An extension is a procedure builder from the package's own initTrpc: its root ctx is what it needs from the app's, its meta type joins the app's (its default meta goes underneath), and the app adds it with t.concat(), the same method a procedure builder takes a plugin with:

// in a package
interface RateLimitMeta {
  
RateLimitMeta.rateLimit?: {
    max: number;
} | undefined
rateLimit
?: { max: numbermax: number };
} const
const r: TrpcRoot<{
    ip: string;
}, RateLimitMeta, RootDef<{
    ip: string;
}, RateLimitMeta>>
r
=
initTrpc<{
    ctx: {
        ip: string;
    };
    meta: RateLimitMeta;
}>(opts?: RootOptions<RateLimitMeta>): TrpcRoot<{
    ip: string;
}, RateLimitMeta, RootDef<{
    ip: string;
}, RateLimitMeta>>
initTrpc
<{
ctx: {
    ip: string;
}
ctx
: { ip: stringip: string }; meta: RateLimitMetameta: RateLimitMeta }>({
RootOptions<RateLimitMeta>.defaultMeta?: RateLimitMeta | undefineddefaultMeta: {
RateLimitMeta.rateLimit?: {
    max: number;
} | undefined
rateLimit
: { max: numbermax: 100 } },
}); export const
const rateLimit: () => ProcedureBuilder<With<With<RootDef<{
    ip: string;
}, RateLimitMeta>, {
    declared: {
        TOO_MANY_REQUESTS: TrpcError<"TOO_MANY_REQUESTS", undefined>;
    };
    errors: TrpcError<"TOO_MANY_REQUESTS", undefined>;
}>, {
    ctx: {
        ip: string;
    };
    ctxAdded: {
        remaining: number;
    };
    inputOut: Unset;
    errors: TrpcError<"TOO_MANY_REQUESTS", undefined>;
    shorts: never;
}>>
rateLimit
= () =>
const r: TrpcRoot<{
    ip: string;
}, RateLimitMeta, RootDef<{
    ip: string;
}, RateLimitMeta>>
r
.
TrpcRoot<{ ip: string; }, RateLimitMeta, RootDef<{ ip: string; }, RateLimitMeta>>.procedure: ProcedureBuilder<RootDef<{
    ip: string;
}, RateLimitMeta>>
procedure
.
ProcedureBuilder<RootDef<{ ip: string; }, RateLimitMeta>>.errors<{
    readonly TOO_MANY_REQUESTS: {};
}>(map: {
    readonly TOO_MANY_REQUESTS: {};
}): ProcedureBuilder<With<RootDef<{
    ip: string;
}, RateLimitMeta>, {
    declared: {
        TOO_MANY_REQUESTS: TrpcError<"TOO_MANY_REQUESTS", undefined>;
    };
    errors: TrpcError<"TOO_MANY_REQUESTS", undefined>;
}>>
errors
({ type TOO_MANY_REQUESTS: {}TOO_MANY_REQUESTS: {} })
.
ProcedureBuilder<With<RootDef<{ ip: string; }, RateLimitMeta>, { declared: { TOO_MANY_REQUESTS: TrpcError<"TOO_MANY_REQUESTS", undefined>; }; errors: TrpcError<"TOO_MANY_REQUESTS", undefined>; }>>.use<{
    remaining: number;
}, TrpcError<"TOO_MANY_REQUESTS", undefined>, never, Unset, {}>(fn: MiddlewareFunction<{
    ip: string;
}, undefined, RateLimitMeta, {
    remaining: number;
}, TrpcError<"TOO_MANY_REQUESTS", undefined>, never, Unset>): ProcedureBuilder<With<With<RootDef<{
    ip: string;
}, RateLimitMeta>, {
    declared: {
        TOO_MANY_REQUESTS: TrpcError<"TOO_MANY_REQUESTS", undefined>;
    };
    errors: TrpcError<"TOO_MANY_REQUESTS", undefined>;
}>, {
    ...;
}>> (+1 overload)
use
((
opts: MiddlewareOpts<{
    ip: string;
}, undefined, RateLimitMeta>
opts
) =>
const limiter: {
    allow: (ip: string, limit?: {
        max: number;
    }) => boolean;
    remaining: (ip: string) => number;
}
limiter
.
allow: (ip: string, limit?: {
    max: number;
}) => boolean
allow
(
opts: MiddlewareOpts<{
    ip: string;
}, undefined, RateLimitMeta>
opts
.
MiddlewareOpts<{ ip: string; }, undefined, RateLimitMeta>.ctx: {
    ip: string;
}
ctx
.ip: stringip,
opts: MiddlewareOpts<{
    ip: string;
}, undefined, RateLimitMeta>
opts
.MiddlewareOpts<{ ip: string; }, undefined, RateLimitMeta>.meta: RateLimitMetameta.
RateLimitMeta.rateLimit?: {
    max: number;
} | undefined
rateLimit
)
?
opts: MiddlewareOpts<{
    ip: string;
}, undefined, RateLimitMeta>
opts
.
MiddlewareOpts<{ ip: string; }, undefined, RateLimitMeta>.next: NextFn
<{
    remaining: number;
}>(opts: {
    ctx: {
        remaining: number;
    };
}) => Promise<MiddlewareResult<{
    remaining: number;
}, Unset>> (+2 overloads)
next
({
ctx: {
    remaining: number;
}
ctx
: { remaining: numberremaining:
const limiter: {
    allow: (ip: string, limit?: {
        max: number;
    }) => boolean;
    remaining: (ip: string) => number;
}
limiter
.remaining: (ip: string) => numberremaining(
opts: MiddlewareOpts<{
    ip: string;
}, undefined, RateLimitMeta>
opts
.
MiddlewareOpts<{ ip: string; }, undefined, RateLimitMeta>.ctx: {
    ip: string;
}
ctx
.ip: stringip) } })
: error<"TOO_MANY_REQUESTS", undefined>(opts: Omit<TrpcErrorOptions<"TOO_MANY_REQUESTS", undefined>, "defined">): TrpcError<"TOO_MANY_REQUESTS", undefined>error({ code: "TOO_MANY_REQUESTS"code: 'TOO_MANY_REQUESTS' }), ); // in the app const
const t: TrpcRoot<{
    ip: string;
    user: User | null;
}, {
    rateLimit?: {
        max: number;
    } | undefined;
}, ExtendedDef<ExtendedDef<RootDef<{
    ip: string;
    user: User | null;
}, {}>, With<With<RootDef<{
    ip: string;
}, RateLimitMeta>, {
    declared: {
        TOO_MANY_REQUESTS: TrpcError<"TOO_MANY_REQUESTS", undefined>;
    };
    errors: TrpcError<"TOO_MANY_REQUESTS", undefined>;
}>, {
    ctx: {
        ip: string;
    };
    ctxAdded: {
        remaining: number;
    };
    inputOut: Unset;
    errors: TrpcError<"TOO_MANY_REQUESTS", undefined>;
    shorts: never;
}>>, RootDef<...>>>
t
=
initTrpc<{
    ctx: {
        ip: string;
        user: User | null;
    };
}>(opts?: RootOptions<{}>): TrpcRoot<{
    ip: string;
    user: User | null;
}, {}, RootDef<{
    ip: string;
    user: User | null;
}, {}>>
initTrpc
<{
ctx: {
    ip: string;
    user: User | null;
}
ctx
: { ip: stringip: string; user: User | nulluser: User | null } }>()
.
TrpcRoot<{ ip: string; user: User | null; }, {}, RootDef<{ ip: string; user: User | null; }, {}>>.concat<With<With<RootDef<{
    ip: string;
}, RateLimitMeta>, {
    declared: {
        TOO_MANY_REQUESTS: TrpcError<"TOO_MANY_REQUESTS", undefined>;
    };
    errors: TrpcError<"TOO_MANY_REQUESTS", undefined>;
}>, {
    ctx: {
        ip: string;
    };
    ctxAdded: {
        remaining: number;
    };
    inputOut: Unset;
    errors: TrpcError<"TOO_MANY_REQUESTS", undefined>;
    shorts: never;
}>>(extension: ProcedureBuilder<With<With<RootDef<{
    ip: string;
}, RateLimitMeta>, {
    declared: {
        TOO_MANY_REQUESTS: TrpcError<"TOO_MANY_REQUESTS", undefined>;
    };
    errors: TrpcError<"TOO_MANY_REQUESTS", undefined>;
}>, {
    ctx: {
        ip: string;
    };
    ctxAdded: {
        remaining: number;
    };
    inputOut: Unset;
    errors: TrpcError<"TOO_MANY_REQUESTS", undefined>;
    shorts: never;
}>>): TrpcRoot<...>
concat
(
const rateLimit: () => ProcedureBuilder<With<With<RootDef<{
    ip: string;
}, RateLimitMeta>, {
    declared: {
        TOO_MANY_REQUESTS: TrpcError<"TOO_MANY_REQUESTS", undefined>;
    };
    errors: TrpcError<"TOO_MANY_REQUESTS", undefined>;
}>, {
    ctx: {
        ip: string;
    };
    ctxAdded: {
        remaining: number;
    };
    inputOut: Unset;
    errors: TrpcError<"TOO_MANY_REQUESTS", undefined>;
    shorts: never;
}>>
rateLimit
())
.
TrpcRoot<{ ip: string; user: User | null; }, { rateLimit?: { max: number; } | undefined; }, ExtendedDef<RootDef<{ ip: string; user: User | null; }, {}>, With<With<RootDef<{ ip: string; }, RateLimitMeta>, { declared: { TOO_MANY_REQUESTS: TrpcError<"TOO_MANY_REQUESTS", undefined>; }; errors: TrpcError<"TOO_MANY_REQUESTS", undefined>; }>, { ...; }>>>.concat<RootDef<{
    user: User | null;
}, {}>>(extension: ProcedureBuilder<RootDef<{
    user: User | null;
}, {}>>): TrpcRoot<{
    ip: string;
    user: User | null;
}, {
    rateLimit?: {
        max: number;
    } | undefined;
}, ExtendedDef<ExtendedDef<RootDef<{
    ip: string;
    user: User | null;
}, {}>, With<With<RootDef<{
    ip: string;
}, RateLimitMeta>, {
    declared: {
        TOO_MANY_REQUESTS: TrpcError<"TOO_MANY_REQUESTS", undefined>;
    };
    errors: TrpcError<"TOO_MANY_REQUESTS", undefined>;
}>, {
    ctx: {
        ip: string;
    };
    ctxAdded: {
        remaining: number;
    };
    inputOut: Unset;
    errors: TrpcError<...>;
    shorts: never;
}>>, RootDef<...>>>
concat
(
const audit: () => ProcedureBuilder<RootDef<{
    user: User | null;
}, {}>>
audit
());
const t: TrpcRoot<{
    ip: string;
    user: User | null;
}, {
    rateLimit?: {
        max: number;
    } | undefined;
}, ExtendedDef<ExtendedDef<RootDef<{
    ip: string;
    user: User | null;
}, {}>, With<With<RootDef<{
    ip: string;
}, RateLimitMeta>, {
    declared: {
        TOO_MANY_REQUESTS: TrpcError<"TOO_MANY_REQUESTS", undefined>;
    };
    errors: TrpcError<"TOO_MANY_REQUESTS", undefined>;
}>, {
    ctx: {
        ip: string;
    };
    ctxAdded: {
        remaining: number;
    };
    inputOut: Unset;
    errors: TrpcError<"TOO_MANY_REQUESTS", undefined>;
    shorts: never;
}>>, RootDef<...>>>
t
.
TrpcRoot<{ ip: string; user: User | null; }, { rateLimit?: { max: number; } | undefined; }, ExtendedDef<ExtendedDef<RootDef<{ ip: string; user: User | null; }, {}>, With<With<RootDef<{ ip: string; }, RateLimitMeta>, { declared: { TOO_MANY_REQUESTS: TrpcError<"TOO_MANY_REQUESTS", undefined>; }; errors: TrpcError<"TOO_MANY_REQUESTS", undefined>; }>, { ...; }>>, RootDef<...>>>.procedure: ProcedureBuilder<ExtendedDef<ExtendedDef<RootDef<{
    ip: string;
    user: User | null;
}, {}>, With<With<RootDef<{
    ip: string;
}, RateLimitMeta>, {
    declared: {
        TOO_MANY_REQUESTS: TrpcError<"TOO_MANY_REQUESTS", undefined>;
    };
    errors: TrpcError<"TOO_MANY_REQUESTS", undefined>;
}>, {
    ctx: {
        ip: string;
    };
    ctxAdded: {
        remaining: number;
    };
    inputOut: Unset;
    errors: TrpcError<"TOO_MANY_REQUESTS", undefined>;
    shorts: never;
}>>, RootDef<{
    user: User | null;
}, {}>>>
procedure
.
ProcedureBuilder<ExtendedDef<ExtendedDef<RootDef<{ ip: string; user: User | null; }, {}>, With<With<RootDef<{ ip: string; }, RateLimitMeta>, { declared: { TOO_MANY_REQUESTS: TrpcError<"TOO_MANY_REQUESTS", undefined>; }; errors: TrpcError<"TOO_MANY_REQUESTS", undefined>; }>, { ...; }>>, RootDef<...>>>.meta<{
    readonly rateLimit: {
        readonly max: 5;
    };
}>(meta: {
    readonly rateLimit: {
        readonly max: 5;
    };
} & UndeclaredMeta<{
    rateLimit?: {
        max: number;
    } | undefined;
}, {
    readonly rateLimit: {
        readonly max: 5;
    };
}>): ProcedureBuilder<ExtendedDef<ExtendedDef<RootDef<{
    ip: string;
    user: User | null;
}, {}>, With<With<RootDef<{
    ip: string;
}, RateLimitMeta>, {
    declared: {
        TOO_MANY_REQUESTS: TrpcError<"TOO_MANY_REQUESTS", undefined>;
    };
    errors: TrpcError<"TOO_MANY_REQUESTS", undefined>;
}>, {
    ctx: {
        ip: string;
    };
    ctxAdded: {
        remaining: number;
    };
    inputOut: Unset;
    errors: TrpcError<...>;
    shorts: never;
}>>, RootDef<...>>>
meta
({
rateLimit: {
    readonly max: 5;
}
rateLimit
: { max: 5max: 5 } })
.
ProcedureBuilder<ExtendedDef<ExtendedDef<RootDef<{ ip: string; user: User | null; }, {}>, With<With<RootDef<{ ip: string; }, RateLimitMeta>, { declared: { TOO_MANY_REQUESTS: TrpcError<"TOO_MANY_REQUESTS", undefined>; }; errors: TrpcError<"TOO_MANY_REQUESTS", undefined>; }>, { ...; }>>, RootDef<...>>>.mutation<number>(resolver: (opts: ResolverOpts<ExtendedDef<ExtendedDef<RootDef<{
    ip: string;
    user: User | null;
}, {}>, With<With<RootDef<{
    ip: string;
}, RateLimitMeta>, {
    declared: {
        TOO_MANY_REQUESTS: TrpcError<"TOO_MANY_REQUESTS", undefined>;
    };
    errors: TrpcError<"TOO_MANY_REQUESTS", undefined>;
}>, {
    ctx: {
        ip: string;
    };
    ctxAdded: {
        remaining: number;
    };
    inputOut: Unset;
    errors: TrpcError<"TOO_MANY_REQUESTS", undefined>;
    shorts: never;
}>>, RootDef<{
    user: User | null;
}, {}>>>) => number): BuildProcedure<...>
mutation
((
opts: ResolverOpts<ExtendedDef<ExtendedDef<RootDef<{
    ip: string;
    user: User | null;
}, {}>, With<With<RootDef<{
    ip: string;
}, RateLimitMeta>, {
    declared: {
        TOO_MANY_REQUESTS: TrpcError<"TOO_MANY_REQUESTS", undefined>;
    };
    errors: TrpcError<"TOO_MANY_REQUESTS", undefined>;
}>, {
    ctx: {
        ip: string;
    };
    ctxAdded: {
        remaining: number;
    };
    inputOut: Unset;
    errors: TrpcError<"TOO_MANY_REQUESTS", undefined>;
    shorts: never;
}>>, RootDef<{
    user: User | null;
}, {}>>>
opts
) =>
opts: ResolverOpts<ExtendedDef<ExtendedDef<RootDef<{
    ip: string;
    user: User | null;
}, {}>, With<With<RootDef<{
    ip: string;
}, RateLimitMeta>, {
    declared: {
        TOO_MANY_REQUESTS: TrpcError<"TOO_MANY_REQUESTS", undefined>;
    };
    errors: TrpcError<"TOO_MANY_REQUESTS", undefined>;
}>, {
    ctx: {
        ip: string;
    };
    ctxAdded: {
        remaining: number;
    };
    inputOut: Unset;
    errors: TrpcError<"TOO_MANY_REQUESTS", undefined>;
    shorts: never;
}>>, RootDef<{
    user: User | null;
}, {}>>>
opts
.
ResolverOpts<ExtendedDef<ExtendedDef<RootDef<{ ip: string; user: User | null; }, {}>, With<With<RootDef<{ ip: string; }, RateLimitMeta>, { declared: { TOO_MANY_REQUESTS: TrpcError<"TOO_MANY_REQUESTS", undefined>; }; errors: TrpcError<"TOO_MANY_REQUESTS", undefined>; }>, { ...; }>>, RootDef<...>>>.ctx: {
    ip: string;
    user: User | null;
    remaining: number;
}
ctx
.remaining: numberremaining);

Extensions chain, and t.middleware and t.implement() see what they add. t.concat() is a type error when the root lacks ctx the extension needs, or declares one of its meta keys with another type. Services an extension provides with .provide() don't need the handler's layer; services it only uses do.

Merging meta

Meta keys a root doesn't declare are a type error, and a later .meta() replaces a key unless its root says how to merge it. An extension sets that for its own keys with initTrpc({ mergeMeta }), which is how OpenAPI's openapi() merges route key by key (OpenAPI). Contracts take extensions too, for their meta: contract.create().concat(openapi()).

Building integrations

@trpc12/trpc/extend has what client integrations build on: createUntypedClient (its options keep link fields in a context bag, beside type, path and input), runLinks, runOperation (runLinks in a client span, with trace propagation), callProcedure, the wire format and JSON Schema helpers.