Chapter 08

中间件进阶

构建日志、缓存、限流中间件,用 Procedure Builder 封装通用业务逻辑

8.1 中间件链:多个 Middleware 按顺序执行

tRPC 中间件通过 .use() 链式组合,按声明顺序执行。每个中间件通过调用 next() 将控制权传递给下一个中间件或最终的 handler。这与 Express middleware 的设计思路一致。

// 中间件执行顺序:logger → isAuthed → handler → isAuthed(回程)→ logger(回程)
const protectedLoggedProcedure = t.procedure
  .use(loggerMiddleware)   // 1. 记录请求开始时间
  .use(isAuthed);          // 2. 检查用户认证
// 每个 .use() 都返回一个新的 Procedure Builder
TS

8.2 日志中间件:记录耗时

const loggerMiddleware = t.middleware(async ({ path, type, next }) => {
  const start = Date.now();
  const result = await next();
  const durationMs = Date.now() - start;

  const meta = { path, type, durationMs };

  if (result.ok) {
    console.log('✅ tRPC OK', meta);
  } else {
    console.error('❌ tRPC Error', { ...meta, error: result.error });
  }

  return result;
});
TS

8.3 Redis 缓存中间件

对于耗时的 Query,可以在中间件层面实现 Redis 缓存,对相同输入直接返回缓存结果:

import { createClient } from 'redis';

const redis = createClient({ url: process.env.REDIS_URL });
await redis.connect();

// 缓存中间件工厂函数(ttl 单位:秒)
const withCache = (ttl: number) =>
  t.middleware(async ({ path, rawInput, next }) => {
    // 只缓存 Query 类型(Mutation 不应该缓存)
    const cacheKey = `trpc:${path}:${JSON.stringify(rawInput)}`;

    const cached = await redis.get(cacheKey);
    if (cached) {
      // 缓存命中,直接返回(注意:需要包装成 tRPC 响应格式)
      return {
        ok: true,
        data: JSON.parse(cached),
        marker: 'middlewareMarker' as const,
      };
    }

    const result = await next();

    if (result.ok) {
      // 缓存成功结果
      await redis.setEx(cacheKey, ttl, JSON.stringify(result.data));
    }

    return result;
  });

// 使用缓存中间件(缓存 5 分钟)
const cachedProcedure = publicProcedure.use(withCache(300));
TS

8.4 限流中间件

防止 API 滥用,基于 IP 或用户 ID 维度限制请求频率:

import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';

const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(10, '10 s'), // 10秒内最多10次
  analytics: true,
});

const rateLimitMiddleware = t.middleware(async ({ ctx, next }) => {
  // 优先使用用户 ID,未登录则用 IP
  const identifier = ctx.user?.id ?? ctx.req.headers['x-forwarded-for'] ?? 'anonymous';

  const { success, limit, remaining, reset } = await ratelimit.limit(
    identifier as string
  );

  if (!success) {
    throw new TRPCError({
      code: 'TOO_MANY_REQUESTS',
      message: `请求过于频繁,请在 ${Math.ceil((reset - Date.now()) / 1000)} 秒后重试`,
    });
  }

  return next({
    ctx: {
      ...ctx,
      // 将限流信息注入 Context,可在 handler 中设置响应头
      rateLimit: { limit, remaining, reset },
    },
  });
});

// 对敏感操作启用限流
export const rateLimitedProcedure = publicProcedure.use(rateLimitMiddleware);
TS

8.5 Procedure Builder 模式:封装通用逻辑

Procedure Builder 是 tRPC 最强大的模式之一。通过链式组合中间件,你可以创建"预装配"的 Procedure 类型,这些类型封装了认证、权限、限流等通用逻辑,业务层代码只需关注业务本身。

// server/trpc.ts — 完整的 Procedure 类型体系
const t = initTRPC.context<Context>().create();

// 基础 Procedure(无限制)
export const publicProcedure = t.procedure
  .use(loggerMiddleware);

// 需要登录的 Procedure
export const protectedProcedure = t.procedure
  .use(loggerMiddleware)
  .use(isAuthed);

// 需要管理员权限的 Procedure
export const adminProcedure = t.procedure
  .use(loggerMiddleware)
  .use(isAuthed)
  .use(requireRole('admin'));

// 有限流的公开 Procedure(适合登录、注册等接口)
export const rateLimitedPublicProcedure = t.procedure
  .use(loggerMiddleware)
  .use(rateLimitMiddleware);

// 带缓存的 Procedure(适合高频读取接口)
export const cachedProcedure = t.procedure
  .use(loggerMiddleware)
  .use(withCache(60));
TS

业务层代码:专注业务,零样板

// server/routers/user.ts
export const userRouter = router({
  // 公开接口,有限流保护
  register: rateLimitedPublicProcedure
    .input(RegisterSchema)
    .mutation(async ({ input }) => { /* ... */ }),

  // 需要登录
  getProfile: protectedProcedure
    .query(async ({ ctx }) => {
      // ctx.user 一定存在,类型安全
      return await ctx.db.user.findUnique({ where: { id: ctx.user.id } });
    }),

  // 管理员功能
  banUser: adminProcedure
    .input(z.object({ userId: z.string() }))
    .mutation(async ({ input, ctx }) => { /* ... */ }),
});
TS

8.6 请求追踪:关联 requestId

import { AsyncLocalStorage } from 'async_hooks';

const requestStorage = new AsyncLocalStorage<{ requestId: string }>();

const requestIdMiddleware = t.middleware(({ ctx, next }) => {
  const requestId = ctx.req.headers['x-request-id']
    ?? crypto.randomUUID();

  return requestStorage.run({ requestId }, () => next({
    ctx: { ...ctx, requestId },
  }));
});

// 在任意地方获取当前请求 ID(用于日志关联)
export const getRequestId = () =>
  requestStorage.getStore()?.requestId ?? 'unknown';
TS
💡

本章小结tRPC 中间件通过 .use() 链式组合,按顺序执行,每个中间件调用 next() 传递控制权。日志、缓存、限流中间件覆盖了生产级应用的核心横切关注点。Procedure Builder 模式是 tRPC 的最佳实践:将认证、权限、限流等通用逻辑封装成不同类型的 Procedure(publicProcedureprotectedProcedureadminProcedure),业务 Router 代码保持简洁,专注业务逻辑。