UNPKG

@trpc/server

Version:

The tRPC server library

442 lines (346 loc) • 13.6 kB
--- name: middlewares description: Create and compose tRPC middleware with t.procedure.use(), extend context via opts.next({ ctx }), build reusable middleware with .concat() and .unstable_pipe(), define base procedures like adminProcedure and protectedProcedure. Read unvalidated input with opts.getRawInput() before .input(), check result.ok from opts.next(). Authorization, logging and timing patterns. requires: - server-setup sources: - www/v11/docs/server/middlewares.md metadata: type: core library: trpc library_version: 11.25.0 --- <!-- Generated from www/v11/docs/server/middlewares.md by www/build/skills.ts. Edit the docs instead. --> # Middlewares From https://trpc.io/docs/server/middlewares You can add middleware(s) to a procedure with the `t.procedure.use()` method. The middleware(s) will wrap the invocation of the procedure and must call `opts.next()` and return its result. ## Authorization In the example below, any call to a `adminProcedure` will ensure that the user is an "admin" before executing. ```ts no-check import { TRPCError, initTRPC } from '@trpc/server'; interface Context { user?: { id: string; isAdmin: boolean; // [..] }; } const t = initTRPC.context<Context>().create(); export const publicProcedure = t.procedure; export const router = t.router; export const adminProcedure = publicProcedure.use(async (opts) => { const { ctx } = opts; if (!ctx.user?.isAdmin) { throw new TRPCError({ code: 'UNAUTHORIZED' }); } return opts.next({ ctx: { user: ctx.user, }, }); }); ``` ```ts no-check import { adminProcedure, publicProcedure, router } from './trpc'; const adminRouter = router({ secretPlace: adminProcedure.query(() => 'a key'), }); export const appRouter = router({ foo: publicProcedure.query(() => 'bar'), admin: adminRouter, }); ``` > [!TIP] > See [Error Handling](https://trpc.io/docs/server/error-handling) to learn more about the `TRPCError` thrown in the above example. ## Logging In the example below timings for queries are logged automatically. ```ts no-check export const loggedProcedure = publicProcedure.use(async (opts) => { const start = Date.now(); const result = await opts.next(); const durationMs = Date.now() - start; const meta = { path: opts.path, type: opts.type, durationMs }; result.ok ? console.log('OK request timing:', meta) : console.error('Non-OK request timing', meta); return result; }); ``` ```ts no-check import { loggedProcedure, router } from './trpc'; export const appRouter = router({ foo: loggedProcedure.query(() => 'bar'), abc: loggedProcedure.query(() => 'def'), }); ``` ## Context Extension "Context Extension" enables middlewares to dynamically add and override keys on a base procedure's context in a typesafe manner. Below we have an example of a middleware that changes properties of a context, the changes are then available to all chained consumers, such as other middlewares and procedures: ```ts no-check type Context = { // user is nullable user?: { id: string; }; }; const protectedProcedure = publicProcedure.use(async function isAuthed(opts) { const { ctx } = opts; // `ctx.user` is nullable if (!ctx.user) { throw new TRPCError({ code: 'UNAUTHORIZED' }); } return opts.next({ ctx: { // ✅ user value is known to be non-null now user: ctx.user, }, }); }); protectedProcedure.query((opts) => { const { ctx } = opts; return ctx.user; }); ``` The `ctx` you pass to `opts.next()` is merged into the existing context, so you only need to pass the keys you add or override. ## Middleware order and input Middlewares run in the order they're chained, and the parser you pass to `.input()` runs at its position in that chain. A middleware chained before `.input()` gets `opts.input` as `undefined`; it can read the unvalidated input with `opts.getRawInput()` instead. ```ts no-check const auditedProcedure = publicProcedure.use(async (opts) => { const rawInput = await opts.getRawInput(); console.log(`${opts.type} ${opts.path}`, rawInput); return opts.next(); }); auditedProcedure .input(z.object({ id: z.string() })) .use((opts) => { // after `.input()`, `opts.input` is parsed and typed console.log(opts.input.id); return opts.next(); }) .query((opts) => opts.input.id); ``` Besides `ctx`, `input`, `getRawInput` and `next`, a middleware receives the procedure's `path`, `type` (`'query'`, `'mutation'` or `'subscription'`), [`meta`](https://trpc.io/docs/server/metadata), and the request's `signal`. ## Using `.concat()` to create reusable middlewares and plugins > [!TIP] > > - Creating middlewares using `t.middleware` has the limitation that the `Context` type is tied to the `Context` type of the tRPC instance. > - Creating middlewares with `experimental_standaloneMiddleware()` has the limitation that you cannot define input parsers and similar tied to your module. tRPC has an API called `.concat()` which allows you to independently define a partial procedure that can be used with any tRPC instance that matches the context and metadata of the plugin. This helper primarily targets creating plugins and libraries with tRPC. ```ts no-check // ------------------------------------------------ // 🧩🧩🧩 a library creating a reusable plugin 🧩🧩🧩 import { initTRPC, TRPCError } from '@trpc/server'; export function createMyPlugin() { // When creating a plugin for tRPC, you use the same API as creating any other tRPC-app // this is the plugin's root `t`-object const t = initTRPC .context<{ // the procedure using the plugin will need to extend this context }>() .meta<{ // the base `initTRPC`-object of the application using this needs to extend this meta }>() .create(); return { // you can also add `.input()` if you want your plugin to do input validation pluginProc: t.procedure.use((opts) => { return opts.next({ ctx: { fromPlugin: 'hello from myPlugin' as const, }, }); }), }; } // ------------------------------------ // 🚀🚀🚀 the app using the plugin 🚀🚀🚀 import { createMyPlugin } from './myPlugin'; import { initTRPC, TRPCError } from '@trpc/server'; // the app's root `t`-object const t = initTRPC .context<{ // ... }>() .create(); export const publicProcedure = t.procedure; export const router = t.router; // initialize the plugin (a real-world example would likely take options here) const plugin = createMyPlugin(); // create a base procedure using the plugin const procedureWithPlugin = publicProcedure .concat( plugin.pluginProc, ) .use(opts => { const { ctx } = opts; return opts.next() }) export const appRouter = router({ hello: procedureWithPlugin.query(opts => { return opts.ctx.fromPlugin; }) }) ``` ## Extending middlewares > [!NOTE] Info > We have prefixed this as `unstable_` as it's a new API, but you're safe to use it! [Read more](https://trpc.io/docs/faq#unstable). We have a powerful feature called `.pipe()` which allows you to extend middlewares in a typesafe manner. Below we have an example of a middleware that extends a base middleware(foo). Like the context extension example above, piping middlewares will change properties of the context, and procedures will receive the new context value. ```ts no-check const fooMiddleware = t.middleware((opts) => { return opts.next({ ctx: { foo: 'foo' as const, }, }); }); const barMiddleware = fooMiddleware.unstable_pipe((opts) => { const { ctx } = opts; ctx.foo; return opts.next({ ctx: { bar: 'bar' as const, }, }); }); const barProcedure = publicProcedure.use(barMiddleware); barProcedure.query((opts) => { const { ctx } = opts; return ctx.bar; }); ``` Beware that the order in which you pipe your middlewares matter and that the context must overlap. An example of a forbidden pipe is shown below. Here, the `fooMiddleware` overrides the `ctx.a` while `barMiddleware` still expects the root context from the initialization in `initTRPC` - so piping `fooMiddleware` with `barMiddleware` would not work, while piping `barMiddleware` with `fooMiddleware` does work. ```ts no-check import { initTRPC } from '@trpc/server'; const t = initTRPC .context<{ a: { b: 'a'; }; }>() .create(); const fooMiddleware = t.middleware((opts) => { const { ctx } = opts; ctx.a; // 👈 fooMiddleware expects `ctx.a` to be an object return opts.next({ ctx: { a: 'a' as const, // 👈 `ctx.a` is no longer an object }, }); }); const barMiddleware = t.middleware((opts) => { const { ctx } = opts; ctx.a; // 👈 barMiddleware expects `ctx.a` to be an object return opts.next({ ctx: { foo: 'foo' as const, }, }); }); // ❌ `ctx.a` does not overlap from `fooMiddleware` to `barMiddleware` fooMiddleware.unstable_pipe(barMiddleware); // ✅ `ctx.a` overlaps from `barMiddleware` and `fooMiddleware` barMiddleware.unstable_pipe(fooMiddleware); ``` ## Common mistakes - **Not returning `opts.next()`.** Every middleware must call `opts.next()` and return its result. Otherwise TypeScript reports an error, and at runtime the call fails with an `INTERNAL_SERVER_ERROR` ("No result from middlewares - did you forget to `return next()`?"). - **Wrapping `opts.next()` in `try`/`catch`.** `opts.next()` doesn't throw when a later middleware or the procedure fails: it resolves to a result with `ok: false` and the `TRPCError`. Check `result.ok`, like the [logging example](https://trpc.io/docs/server/middlewares#logging) does: ```ts no-check const reportedProcedure = publicProcedure.use(async (opts) => { const result = await opts.next(); if (!result.ok) { console.error(`${opts.path} failed`, result.error); } return result; }); ``` ## Experimental: standalone middlewares > [!WARNING] Deprecated > This has been deprecated in favor of [`.concat()`](https://trpc.io/docs/server/middlewares#concat). Use `.concat()` for new code. tRPC has an experimental API called `experimental_standaloneMiddleware` which allows you to independently define a middleware that can be used with any tRPC instance. Creating middlewares using `t.middleware` has the limitation that the `Context` type is tied to the `Context` type of the tRPC instance. This means that you cannot use the same middleware with multiple tRPC instances that have different `Context` types. Using `experimental_standaloneMiddleware` you can create a middleware that explicitly defines its requirements, i.e. the Context, Input and Meta types: ```ts no-check import { experimental_standaloneMiddleware, initTRPC, TRPCError, } from '@trpc/server'; import * as z from 'zod'; const projectAccessMiddleware = experimental_standaloneMiddleware<{ ctx: { allowedProjects: string[] }; // defaults to 'object' if not defined input: { projectId: string }; // defaults to 'unknown' if not defined // 'meta', not defined here, defaults to 'object | undefined' }>().create((opts) => { if (!opts.ctx.allowedProjects.includes(opts.input.projectId)) { throw new TRPCError({ code: 'FORBIDDEN', message: 'Not allowed', }); } return opts.next(); }); const t1 = initTRPC .context<{ allowedProjects: string[]; }>() .create(); // ✅ `ctx.allowedProjects` satisfies "string[]" and `input.projectId` satisfies "string" const accessControlledProcedure = t1.procedure .input(z.object({ projectId: z.string() })) .use(projectAccessMiddleware); // ❌ `ctx.allowedProjects` satisfies "string[]" but `input.projectId` does not satisfy "string" const accessControlledProcedure2 = t1.procedure .input(z.object({ projectId: z.number() })) .use(projectAccessMiddleware); // ❌ `ctx.allowedProjects` does not satisfy "string[]" even though `input.projectId` satisfies "string" const t2 = initTRPC .context<{ allowedProjects: number[]; }>() .create(); const accessControlledProcedure3 = t2.procedure .input(z.object({ projectId: z.string() })) .use(projectAccessMiddleware); ``` Here is an example with multiple standalone middlewares: ```ts no-check import { experimental_standaloneMiddleware, initTRPC } from '@trpc/server'; import * as z from 'zod'; const t = initTRPC.create(); const schemaA = z.object({ valueA: z.string() }); const schemaB = z.object({ valueB: z.string() }); const valueAUppercaserMiddleware = experimental_standaloneMiddleware<{ input: z.infer<typeof schemaA>; }>().create((opts) => { return opts.next({ ctx: { valueAUppercase: opts.input.valueA.toUpperCase() }, }); }); const valueBUppercaserMiddleware = experimental_standaloneMiddleware<{ input: z.infer<typeof schemaB>; }>().create((opts) => { return opts.next({ ctx: { valueBUppercase: opts.input.valueB.toUpperCase() }, }); }); const combinedInputThatSatisfiesBothMiddlewares = z.object({ valueA: z.string(), valueB: z.string(), extraProp: z.string(), }); t.procedure .input(combinedInputThatSatisfiesBothMiddlewares) .use(valueAUppercaserMiddleware) .use(valueBUppercaserMiddleware) .query( ({ input: { valueA, valueB, extraProp }, ctx: { valueAUppercase, valueBUppercase }, }) => `valueA: ${valueA}, valueB: ${valueB}, extraProp: ${extraProp}, valueAUppercase: ${valueAUppercase}, valueBUppercase: ${valueBUppercase}`, ); ``` ## Related skills - `server-setup` (https://trpc.io/docs/server/overview): `npx @tanstack/intent@latest load @trpc/server#server-setup`