自定义中间件链
为日志记录、速率限制或授权构建自定义中间件,并将它们串联起来。
自定义中间件链 是 CoddyKit 上的免费 tRPC End-to-End Type Safe APIs 课时。 这是第 3 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 tRPC End-to-End Type Safe APIs 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 tRPC End-to-End Type Safe APIs 课程共包含 4 节课。
本课时的部分内容尚未翻译,以英文显示。
Chain Multiple Middleware
In tRPC, middleware lets you run logic before your procedure executes. What if you need multiple checks, like logging, authentication, and validation, for a single API call?
This is where middleware chains come in handy! They allow you to stack multiple middleware functions, executing them one after another.
Benefits of Chaining
Chaining middleware offers several advantages:
- Modularity: Each middleware focuses on a single responsibility (e.g., logging, auth).
- Reusability: Create generic middleware that can be applied to different procedures or routers.
- Order of Execution: Control the exact sequence of operations before your main procedure logic runs.
- Early Exit: A middleware can stop the chain early if a condition isn't met (e.g., unauthorized access).
How `next()` Works
Recall that tRPC middleware receives a next function. Calling next() passes control to the next middleware in the chain, or to the actual procedure handler if it's the last middleware.
If a middleware doesn't call next(), the chain stops, and the procedure handler will not execute. This is crucial for checks like authentication!
First: Logging Middleware
Let's create a simple logging middleware. It will log when a request starts and how long it took. This is a common pattern for monitoring.
Notice how next() is awaited to ensure the procedure (and subsequent middleware) completes before logging the duration.
type Context = { userId: string | null };
const trpc = {
middleware: (handler: any) => handler,
procedure: {
use: (mw: any) => ({
query: (f: any) => f,
mutation: (f: any) => f
}),
query: (f: any) => f,
mutation: (f: any) => f
},
router: (cfg: any) => cfg,
};
const logMiddleware = trpc.middleware(async ({ path, type, next }) => {
console.log(`[${type}] Request to ${path} started.`);
const start = Date.now();
const result = await next();
const durationMs = Date.now() - start;
console.log(`[${type}] Request to ${path} finished in ${durationMs}ms.`);
return result;
});
const appRouter = trpc.router({
example: trpc.procedure
.use(logMiddleware)
.query(() => "Data fetched!")
});Second: Auth Middleware
Next, we'll build an authentication middleware. This middleware will check if a user is logged in (i.e., if ctx.userId exists). If not, it will throw an error, effectively stopping the procedure.
Remember, the ctx (context) object is passed to middleware, allowing access to request-specific data.
type Context = { userId: string | null };
const trpc = {
middleware: (handler: any) => handler,
procedure: {
use: (mw: any) => ({
query: (f: any) => f,
mutation: (f: any) => f
}),
query: (f: any) => f,
mutation: (f: any) => f
},
router: (cfg: any) => cfg,
};
const isAuthenticated = trpc.middleware(async ({ ctx, next }) => {
if (!ctx.userId) {
throw new Error("UNAUTHORIZED: Please log in.");
}
return next();
});
const appRouter = trpc.router({
protected: trpc.procedure
.use(isAuthenticated)
.query(({ ctx }) => `Welcome, user ${ctx.userId}!`)
});Building a Middleware Chain
Now, let's combine our logMiddleware and isAuthenticated middleware. The order matters: logging should happen first, then authentication.
We simply use .use() multiple times. tRPC will execute them in the order they are defined.
type Context = { userId: string | null };
const trpc = {
middleware: (handler: any) => handler,
procedure: {
use: (mw: any) => ({
query: (f: any) => f,
mutation: (f: any) => f
}),
query: (f: any) => f,
mutation: (f: any) => f
},
router: (cfg: any) => cfg,
};
const logMiddleware = trpc.middleware(async ({ path, type, next }) => {
console.log(`[${type}] Request to ${path} started.`);
const start = Date.now();
const result = await next();
const durationMs = Date.now() - start;
console.log(`[${type}] Request to ${path} finished in ${durationMs}ms.`);
return result;
});
const isAuthenticated = trpc.middleware(async ({ ctx, next }) => {
if (!ctx.userId) {
throw new Error("UNAUTHORIZED: Please log in.");
}
return next();
});
const appRouter = trpc.router({
protectedData: trpc.procedure
.use(logMiddleware)
.use(isAuthenticated)
.query(({ ctx }) => `Secret data for ${ctx.userId}!`)
});Order of Execution
When a request hits protectedData:
logMiddlewareruns first.- If
logMiddlewarecallsnext(), thenisAuthenticatedruns. - If
isAuthenticatedcallsnext(), then the actualqueryprocedure handler runs. - If any middleware throws an error (like
isAuthenticated), the chain stops immediately, and the error is returned.
Dynamic Middleware
What if you need different authorization levels? You can create a function that returns a middleware, allowing for dynamic configuration.
This pattern is called a middleware builder and is powerful for creating flexible and reusable middleware.
type Context = { userId: string | null; userRole: string | null };
const trpc = {
middleware: (handler: any) => handler,
procedure: {
use: (mw: any) => ({
query: (f: any) => f,
mutation: (f: any) => f
}),
query: (f: any) => f,
mutation: (f: any) => f
},
router: (cfg: any) => cfg,
};
const hasRole = (requiredRole: string) => {
return trpc.middleware(async ({ ctx, next }) => {
if (!ctx.userRole || ctx.userRole !== requiredRole) {
throw new Error(`FORBIDDEN: ${requiredRole} role required.`);
}
return next();
});
};
const appRouter = trpc.router({
adminPanel: trpc.procedure
.use(hasRole("admin"))
.query(() => "Welcome, admin!")
});Chaining Dynamic Middleware
You can mix and match static middleware (like our logger) with dynamic middleware builders (like our role checker) in the same chain.
This allows for highly flexible and secure API endpoints, where specific logic can be applied based on configuration or runtime values.
type Context = { userId: string | null; userRole: string | null };
const trpc = {
middleware: (handler: any) => handler,
procedure: {
use: (mw: any) => ({
query: (f: any) => f,
mutation: (f: any) => f
}),
query: (f: any) => f,
mutation: (f: any) => f
},
router: (cfg: any) => cfg,
};
const logMiddleware = trpc.middleware(async ({ path, type, next }) => {
console.log(`[${type}] Request to ${path} started.`);
const start = Date.now();
const result = await next();
const durationMs = Date.now() - start;
console.log(`[${type}] Request to ${path} finished in ${durationMs}ms.`);
return result;
});
const hasRole = (requiredRole: string) => {
return trpc.middleware(async ({ ctx, next }) => {
if (!ctx.userRole || ctx.userRole !== requiredRole) {
throw new Error(`FORBIDDEN: ${requiredRole} role required.`);
}
return next();
});
};
const appRouter = trpc.router({
adminReport: trpc.procedure
.use(logMiddleware)
.use(hasRole("admin"))
.query(() => "Sensitive admin report data.")
});Middleware Chain Logic
Consider the following tRPC procedure definition:
// Assume logMiddleware and authMiddleware are defined
// authMiddleware throws if ctx.userId is null
// logMiddleware always calls next()
const myProcedure = trpc.procedure
.use(logMiddleware)
.use(authMiddleware)
.query(({ ctx }) => `Hello ${ctx.userId}`);If a request comes in with ctx.userId = null, what will happen?
Chains Recap
Great job! You've learned how to build and chain multiple custom middleware in tRPC.
- Middleware chains allow you to apply multiple layers of logic (logging, authentication, validation) before a procedure executes.
- The
.use()method is used to add middleware, and they run in the order they are defined. - The
next()function is key to passing control down the chain. - Middleware builders can create dynamic, configurable middleware for enhanced reusability.
This powerful pattern helps keep your tRPC API code clean, modular, and robust!
常见问题解答
「自定义中间件链」课时是免费的吗?
是的 — 「自定义中间件链」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 tRPC End-to-End Type Safe APIs 课程的其余内容,请升级到 CoddyKit PRO。 tRPC End-to-End Type Safe APIs 课程共包含 4 节课。
「自定义中间件链」这节课中我会学到什么?
为日志记录、速率限制或授权构建自定义中间件,并将它们串联起来。 你通过在浏览器中直接运行的动手代码来练习 tRPC End-to-End Type Safe APIs,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 tRPC End-to-End Type Safe APIs 需要有经验吗?
无需任何先前经验。CoddyKit 上的 tRPC End-to-End Type Safe APIs 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 3 节课,共 4 节。
「自定义中间件链」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 tRPC End-to-End Type Safe APIs 课中编写并运行代码吗?
能。每节 tRPC End-to-End Type Safe APIs 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。