trpc-router

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

TRPC Router Guide

TRPC 路由指南

File Location

文件位置

  • Routers:
    src/server/routers/lambda/<domain>.ts
  • Helpers:
    src/server/routers/lambda/_helpers/
  • Schemas:
    src/server/routers/lambda/_schema/
  • 路由文件:
    src/server/routers/lambda/<domain>.ts
  • 辅助工具:
    src/server/routers/lambda/_helpers/
  • 校验Schema:
    src/server/routers/lambda/_schema/

Router Structure

路由结构

Imports

导入

typescript
import { TRPCError } from '@trpc/server';
import { z } from 'zod';

import { SomeModel } from '@/database/models/some';
import { authedProcedure, router } from '@/libs/trpc/lambda';
import { serverDatabase } from '@/libs/trpc/lambda/middleware';
typescript
import { TRPCError } from '@trpc/server';
import { z } from 'zod';

import { SomeModel } from '@/database/models/some';
import { authedProcedure, router } from '@/libs/trpc/lambda';
import { serverDatabase } from '@/libs/trpc/lambda/middleware';

Middleware: Inject Models into ctx

中间件:将模型注入ctx

Always use middleware to inject models into
ctx
instead of creating
new Model(ctx.serverDB, ctx.userId)
inside every procedure.
typescript
const domainProcedure = authedProcedure.use(serverDatabase).use(async (opts) => {
  const { ctx } = opts;
  return opts.next({
    ctx: {
      fooModel: new FooModel(ctx.serverDB, ctx.userId),
      barModel: new BarModel(ctx.serverDB, ctx.userId),
    },
  });
});
Then use
ctx.fooModel
in procedures:
typescript
// Good
const model = ctx.fooModel;

// Bad - don't create models inside procedures
const model = new FooModel(ctx.serverDB, ctx.userId);
Exception: When a model needs a different
userId
(e.g., watchdog iterating over multiple users' tasks), create it inline.
请始终使用中间件将模型注入到
ctx
,不要在每个procedure内部创建
new Model(ctx.serverDB, ctx.userId)
实例。
typescript
const domainProcedure = authedProcedure.use(serverDatabase).use(async (opts) => {
  const { ctx } = opts;
  return opts.next({
    ctx: {
      fooModel: new FooModel(ctx.serverDB, ctx.userId),
      barModel: new BarModel(ctx.serverDB, ctx.userId),
    },
  });
});
之后你可以在procedure中直接使用
ctx.fooModel
typescript
// Good
const model = ctx.fooModel;

// Bad - don't create models inside procedures
const model = new FooModel(ctx.serverDB, ctx.userId);
例外情况:如果模型需要使用不同的
userId
(例如监控程序遍历多个用户的任务),可以在procedure内直接创建实例。

Procedure Pattern

Procedure编写范式

typescript
export const fooRouter = router({
  // Query
  find: domainProcedure.input(z.object({ id: z.string() })).query(async ({ input, ctx }) => {
    try {
      const item = await ctx.fooModel.findById(input.id);
      if (!item) throw new TRPCError({ code: 'NOT_FOUND', message: 'Not found' });
      return { data: item, success: true };
    } catch (error) {
      if (error instanceof TRPCError) throw error;
      console.error('[foo:find]', error);
      throw new TRPCError({
        cause: error,
        code: 'INTERNAL_SERVER_ERROR',
        message: 'Failed to find item',
      });
    }
  }),

  // Mutation
  create: domainProcedure.input(createSchema).mutation(async ({ input, ctx }) => {
    try {
      const item = await ctx.fooModel.create(input);
      return { data: item, message: 'Created', success: true };
    } catch (error) {
      if (error instanceof TRPCError) throw error;
      console.error('[foo:create]', error);
      throw new TRPCError({
        cause: error,
        code: 'INTERNAL_SERVER_ERROR',
        message: 'Failed to create',
      });
    }
  }),
});
typescript
export const fooRouter = router({
  // Query
  find: domainProcedure.input(z.object({ id: z.string() })).query(async ({ input, ctx }) => {
    try {
      const item = await ctx.fooModel.findById(input.id);
      if (!item) throw new TRPCError({ code: 'NOT_FOUND', message: 'Not found' });
      return { data: item, success: true };
    } catch (error) {
      if (error instanceof TRPCError) throw error;
      console.error('[foo:find]', error);
      throw new TRPCError({
        cause: error,
        code: 'INTERNAL_SERVER_ERROR',
        message: 'Failed to find item',
      });
    }
  }),

  // Mutation
  create: domainProcedure.input(createSchema).mutation(async ({ input, ctx }) => {
    try {
      const item = await ctx.fooModel.create(input);
      return { data: item, message: 'Created', success: true };
    } catch (error) {
      if (error instanceof TRPCError) throw error;
      console.error('[foo:create]', error);
      throw new TRPCError({
        cause: error,
        code: 'INTERNAL_SERVER_ERROR',
        message: 'Failed to create',
      });
    }
  }),
});

Aggregated Detail Endpoint

聚合详情端点

For views that need multiple related data, create a single
detail
procedure that fetches everything in parallel:
typescript
detail: domainProcedure.input(idInput).query(async ({ input, ctx }) => {
  const item = await resolveOrThrow(ctx.fooModel, input.id);

  const [children, related] = await Promise.all([
    ctx.fooModel.findChildren(item.id),
    ctx.barModel.findByFooId(item.id),
  ]);

  return {
    data: { ...item, children, related },
    success: true,
  };
}),
This avoids the CLI or frontend making N sequential requests.
对于需要多个关联数据的视图,可创建单个
detail
procedure并行拉取所有所需数据:
typescript
detail: domainProcedure.input(idInput).query(async ({ input, ctx }) => {
  const item = await resolveOrThrow(ctx.fooModel, input.id);

  const [children, related] = await Promise.all([
    ctx.fooModel.findChildren(item.id),
    ctx.barModel.findByFooId(item.id),
  ]);

  return {
    data: { ...item, children, related },
    success: true,
  };
}),
这样可以避免CLI或前端发起N个串行请求。

Conventions

开发规范

  • Return shape:
    { data, success: true }
    for queries,
    { data?, message, success: true }
    for mutations
  • Error handling: re-throw
    TRPCError
    , wrap others with
    console.error
    + new
    TRPCError
  • Input validation: use
    zod
    schemas, define at file top
  • Router name:
    export const fooRouter = router({ ... })
  • Procedure names: alphabetical order within the router object
  • Log prefix:
    [domain:procedure]
    format, e.g.
    [task:create]
  • 返回格式:查询类请求返回
    { data, success: true }
    ,变更类请求返回
    { data?, message, success: true }
  • 错误处理:遇到
    TRPCError
    直接抛出,其他错误需先通过
    console.error
    打印日志,再封装为新的
    TRPCError
    抛出
  • 输入校验:使用
    zod
    schema,在文件顶部定义
  • 路由命名:遵循
    export const fooRouter = router({ ... })
    格式
  • Procedure命名:在路由对象内按字母顺序排列
  • 日志前缀:使用
    [领域:procedure名称]
    格式,例如
    [task:create]