@trpc/server
Version:
442 lines (346 loc) • 13.6 kB
Markdown
---
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.23.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`