You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在tRPC中合理使用上下文?多路由DRY实现方案咨询

问题描述

我有一个基于tRPC的基础API,包含两组端点:

  • 用于查询和修改User属性的端点,需要username参数
  • 用于查询和修改Post属性的端点,需要post ID参数

暂不考虑认证,我尝试为每组路由设置独立Context,但合并路由时出现上下文报错,tRPC文档也未提及该场景的实现方法。用中间件的话,每个查询/ mutation都要重复定义{ input: z.string() }或{ input: z.number() },不符合DRY原则。请问最优解决方案是什么?

尝试的代码示例:

// post.ts
export async function createContext(
    opts?: trpcExpress.CreateExpressContextOptions
) {
    // pass through post id, throw if not present
}
type Context = trpc.inferAsyncReturnType<typeof createContext>;

const router = trpc
    .router()
    .query("get", {
        resolve(req) {
            // get post from database
            return post;
        },
    });

// similar thing in user.ts

// server.ts
const trpcRouter = trpc
    .router()
    .merge("post.", postRouter)
    .merge("user.", userRouter);

app.use(
    "/trpc",
    trpcExpress.createExpressMiddleware({
        router: trpcRouter,
        createContext,
    })
);
最优解决方案

采用路由级中间件+复用输入Schema的方式,既能实现路由专属的上下文注入,又能避免重复定义输入验证逻辑,完全符合DRY原则。

步骤1:定义通用输入Schema

把User和Post的基础输入规则抽离成可复用的Zod Schema:

// schemas.ts
import { z } from "zod";

// Post路由的基础输入(必传postId)
export const postBaseInput = z.object({
  postId: z.string().uuid(), // 可根据实际ID格式调整
});

// User路由的基础输入(必传username)
export const userBaseInput = z.object({
  username: z.string().min(3),
});

步骤2:创建路由专属中间件

为Post和User路由分别编写中间件,负责验证输入、查询对应资源并注入上下文:

// post.ts
import { trpc } from "../trpc";
import { postBaseInput } from "../schemas";
import { db } from "../db"; // 你的数据库实例

// Post路由专属中间件:验证postId并查询Post注入上下文
const postMiddleware = trpc.middleware(async ({ input, next }) => {
  // 验证输入,自动抛出验证错误
  const { postId } = postBaseInput.parse(input);
  
  // 查询Post资源
  const post = await db.post.findUnique({
    where: { id: postId },
  });
  if (!post) throw new Error("Post不存在");

  // 将Post注入上下文,传递给后续路由处理函数
  return next({
    ctx: {
      post,
    },
  });
});

// 创建Post路由,挂载专属中间件
export const postRouter = trpc
  .router()
  .middleware(postMiddleware)
  // 查询Post:直接使用上下文的post,无需重复查询
  .query("get", {
    resolve({ ctx }) {
      return ctx.post;
    },
  })
  // 更新Post:合并基础输入和额外字段,无需重复定义postId
  .mutation("updateContent", {
    input: postBaseInput.extend({
      content: z.string().min(1),
    }),
    resolve({ ctx, input }) {
      return db.post.update({
        where: { id: ctx.post.id },
        data: { content: input.content },
      });
    },
  });

User路由的实现逻辑完全一致:

// user.ts
import { trpc } from "../trpc";
import { userBaseInput } from "../schemas";
import { db } from "../db";

const userMiddleware = trpc.middleware(async ({ input, next }) => {
  const { username } = userBaseInput.parse(input);
  
  const user = await db.user.findUnique({
    where: { username },
  });
  if (!user) throw new Error("用户不存在");

  return next({
    ctx: {
      user,
    },
  });
});

export const userRouter = trpc
  .router()
  .middleware(userMiddleware)
  .query("get", {
    resolve({ ctx }) {
      return ctx.user;
    },
  })
  .mutation("updateEmail", {
    input: userBaseInput.extend({
      email: z.string().email(),
    }),
    resolve({ ctx, input }) {
      return db.user.update({
        where: { username: ctx.user.username },
        data: { email: input.email },
      });
    },
  });

步骤3:合并路由并启动服务

主路由直接合并两个子路由,基础上下文只需返回空对象即可,子路由的中间件会自动补充专属上下文:

// server.ts
import { trpc } from "./trpc";
import { postRouter } from "./post";
import { userRouter } from "./user";
import { trpcExpress } from "@trpc/server/adapters/express";
import express from "express";

const app = express();

const appRouter = trpc
  .router()
  .merge("post.", postRouter)
  .merge("user.", userRouter);

app.use(
  "/trpc",
  trpcExpress.createExpressMiddleware({
    router: appRouter,
    createContext: () => ({}), // 基础上下文为空,路由中间件补充专属内容
  })
);

app.listen(3000, () => {
  console.log("服务运行在 http://localhost:3000");
});

方案优势

  • 符合DRY原则:基础输入Schema只定义一次,路由中间件统一处理验证和资源查询,子路由无需重复代码。
  • 路由专属上下文:每个子路由通过中间件注入专属的资源(Post/User),避免上下文混乱。
  • 类型安全:Zod验证保证输入合法性,TypeScript自动推导上下文类型,避免类型错误。

内容的提问来源于stack exchange,提问作者Christian Legge

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.19 15:35:29