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): Procedure<...>
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 sees what they add. examples/saas-api has two: a tenancy extension that adds the caller's tenant and role, and an audit extension that starts with the same session middleware, so it runs once. 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 }).
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.