- 后端
- 前端
- CRM
- 人工智能
- AI Agent
【免费下载链接】crm
Comp AI CRM is an open source, CRM designed for AI agents. Agentic-first CRM.
TRPCModule.forRoot()是 nestjs-trpc 在 NestJS 中接入 tRPC 的唯一入口,其选项决定了路由挂载点、请求上下文、全局中间件、错误处理、数据序列化与流式传输等全部行为。本文基于 nestjs-trpc v2.13.0 发布的dist/interfaces/module-options.interface.d.ts逐项拆解TRPCModuleOptions的每个字段,并结合 Comp AI CRM 仓库 apps/api/src/trpc/trpc.module.ts 中的真实生产配置,给出可直接复制的配置写法与避坑要点。读完本文你将掌握:如何正确装配 forRoot、如何用全局中间件与 onError 做可观测性、如何用 errorFormatter 塑造客户端看到的错误载荷,以及 SSE / JSONL 流式场景下的超时与保活调优。
TRPCModuleOptions 全景:一份类型定义看懂全部选项
TRPCModuleOptions的类型定义被完整转写自 v2.13.0 发布的dist/interfaces/module-options.interface.d.ts:
export interface TRPCModuleOptions { basePath?: string; context?: Class<TRPCContext>; errorFormatter?: TRPCErrorFormatter<any, TRPCDefaultErrorShape>; transformer?: DataTransformer | CombinedDataTransformer; logger?: LoggerService; onError?: Class<TRPCErrorHandler>; globalMiddlewares?: Array<Class<TRPCMiddleware> | Constructor<TRPCMiddleware>>; sse?: TRPCSSEOptions; jsonl?: TRPCJSONLOptions; }各选项的类型与默认值汇总如下:
| Option | Type | Default |
|---|---|---|
basePath | string | "/trpc" |
context | Class<TRPCContext> | — |
errorFormatter | TRPCErrorFormatter | — |
transformer | DataTransformer \| CombinedDataTransformer | — |
logger | LoggerService | ConsoleLogger |
onError | Class<TRPCErrorHandler> | — |
globalMiddlewares | Array<Class<TRPCMiddleware>> | [] |
sse | TRPCSSEOptions | 见下文说明 |
jsonl | TRPCJSONLOptions | — |
两条贯穿全文的硬性规则,请先记住:
TRPCModule.forRoot(options?)返回的是DynamicModule,且没有forRootAsync。如果配置依赖运行时值(如数据库连接、环境变量),正确做法是把ConfigService注入到 context 类或中间件里,而不是试图在模块定义阶段await任何东西。- 所有以类形式传入的选项(
context、onError、globalMiddlewares中的类)必须同时出现在模块的providers数组里。forRoot只是记录“用哪个类”,真正让 Nest 完成实例化与依赖注入的是providers。漏掉任何一个,运行时就会得到未定义注入或“procedure 不存在”的 404。
这一点在 Comp AI CRM 中体现得十分标准:apps/api/src/trpc/trpc.module.ts 里forRoot传入的TrpcContext、TrpcErrorHandler、LoggingMiddleware、DomainErrorMiddleware四个类全部重复列入了providers。
basePath:路由挂载点与 404 的头号来源
basePath决定 tRPC handler 挂载在哪个路径下。默认值是/trpc,此时每个 procedure 的完整地址为/trpc/<alias>.<procedure>:
TRPCModule.forRoot({ basePath: '/api/trpc' })两个最容易踩的坑:
- 必须与客户端
httpLink配置的url完全一致,包括你用app.setGlobalPrefix()设置的全局前缀。两者是拼接关系:basePath与全局前缀会组合成最终路径,忘记这一点是“看起来像 procedure 不存在”的 404 最常见成因。 - 如果客户端指向了错误的路径,你会在网络面板看到一个 404,而 tRPC 侧查不到任何 procedure 调用记录——先核对两端 URL 再排查逻辑。
Comp AI CRM 的实践是basePath: '/api/trpc'(见 apps/api/src/trpc/trpc.module.ts),并在错误处理中保留了/trpc/${path}的路径还原逻辑(见 apps/api/src/trpc/trpc-error.handler.ts),方便遥测系统还原被全局前缀修饰前的原始 procedure 路径。
context:每个请求一次的自定义上下文
context接收一个实现TRPCContext接口的类,该类的实例每个请求创建一次,是挂载请求对象、会话、按请求粒度的数据加载器的正确位置。接口约定与实现细节可进一步阅读 middlewares-and-context.md。
Comp AI CRM 的实现展示了标准写法(apps/api/src/trpc/trpc.context.ts):
import { auth } from "@crm/auth"; import { Injectable } from "@nestjs/common"; import { fromNodeHeaders } from "better-auth/node"; import type { Request } from "express"; import type { ContextOptions, TRPCContext } from "nestjs-trpc"; import type { BaseTrpcContext } from "./context.types"; export async function createBaseTrpcContext( req: Request | undefined, ): Promise<BaseTrpcContext> { const session = req ? await auth.api .getSession({ headers: fromNodeHeaders(req.headers) }) .catch(() => null) : null; return { req, session }; } @Injectable() export class TrpcContext implements TRPCContext { async create(opts: ContextOptions): Promise<BaseTrpcContext> { const req = "req" in opts ? opts.req : undefined; return createBaseTrpcContext(req); } }从源码结构可以看到几个值得借鉴的要点:
- 上下文类型在 context.types.ts 中单独定义,
BaseTrpcContext包含req与session,并派生出带user的AuthedTrpcContext供认证后的 procedure 使用——类型在中间件链中逐层收窄,而不是在上下文里塞一个大而全的对象。 - session 解析失败时用
.catch(() => null)兜底,保证未登录请求也能正常进入后续流程,由认证中间件决定是否拒绝。 "req" in opts的存在性判断是为了兼容 Express 与 Fastify 两套ContextOptions结构,保证上下文类在两个平台下都能工作。
globalMiddlewares:全局中间件与“不要放认证”的告诫
globalMiddlewares中的中间件会应用到应用内每一个procedure,且执行顺序在 router 级与 procedure 级中间件之前:
@Module({ imports: [ TRPCModule.forRoot({ globalMiddlewares: [LoggedMiddleware, ErrorReportingMiddleware], }), ], providers: [LoggedMiddleware, ErrorReportingMiddleware], }) export class AppModule {}适合放进全局中间件的场景:计时、日志、错误上报、限流。不适合的场景是认证——因为全局认证中间件必须为每一个公开 procedure 特判放行,而这些例外很容易写错、漏掉,导致公开接口意外被锁死或受保护接口意外暴露。更好的做法是:在需要保护的 router 上显式标注@UseMiddlewares(AuthMiddleware),让受保护面在代码里一目了然。
Comp AI CRM 的全局中间件组合(apps/api/src/trpc/trpc.module.ts)恰好体现了“全局做可观测、局部做认证”的分工:
LoggingMiddleware(apps/api/src/trpc/middlewares/logging.middleware.ts)在next()前后记录耗时,输出type path ok/err durationMs的结构化日志。注意它用Date.now()而不是process.hrtime,毫秒精度对日志足够;关键点是始终returnopts.next()的结果,否则中间件链会在这一层被吞掉。DomainErrorMiddleware(apps/api/src/trpc/middlewares/domain-error.middleware.ts)把业务代码抛出的 NestJSHttpException翻译成 tRPC 的TRPCError:通过 HTTP 状态码(400/401/403/404/409/429)映射到BAD_REQUEST/UNAUTHORIZED/FORBIDDEN/NOT_FOUND/CONFLICT/TOO_MANY_REQUESTS,其余一律归为INTERNAL_SERVER_ERROR。这解决了一个关键问题:默认情况下HttpException不会被翻译成 tRPC 错误形状,会以不透明的 500 浮出表面。
AuthMiddleware与SessionOnlyMiddleware并没有放进globalMiddlewares,而是作为 providers 被模块exports,由各个 router 按需引用——这正是原文档建议的“显式认证”模式。
onError:只观察、不改写的错误上报钩子
onError是每次 procedure 抛错时被调用的可注入 handler,典型用途是上报 Sentry 等监控系统。关键约束:它只能观察,不能改写响应。
// app.error-handler.ts import { Inject, Injectable } from '@nestjs/common'; import { OnErrorOptions, TRPCErrorHandler } from 'nestjs-trpc'; @Injectable() export class AppErrorHandler implements TRPCErrorHandler { constructor(@Inject(LogService) private readonly logService: LogService) {} onError(opts: OnErrorOptions): void { this.logService.error(`[${opts.type}] ${opts.path}: ${opts.error.message}`); } }OnErrorOptions与TRPCErrorHandler的完整接口如下:
export interface OnErrorOptions { error: TRPCError; type: TRPCProcedureType | 'unknown'; path: string | undefined; input: unknown; ctx: Record<string, unknown> | undefined; req: unknown; } export interface TRPCErrorHandler { onError(opts: OnErrorOptions): void; }装配方式同样是“forRoot 注册 + providers 提供实例”:
@Module({ imports: [TRPCModule.forRoot({ onError: AppErrorHandler })], providers: [AppErrorHandler], }) export class AppModule {}三条使用纪律:
onError返回void且不会被 await——不要把慢操作(如同步发送邮件、磁盘写入)放在请求路径上。opts.input是用户的原始载荷,opts.ctx可能携带会话数据;两者都会随你的上报到达目标系统,受数据合规约束时务必先脱敏再落日志。- 要区分真实 bug 与预期拒绝,按错误码过滤——一个打错 id 的
NOT_FOUND不是事故:
onError(opts: OnErrorOptions): void { if (opts.error.code === 'INTERNAL_SERVER_ERROR') { this.sentry.captureException(opts.error.cause ?? opts.error, { extra: { path: opts.path } }); } }Comp AI CRM 的 trpc-error.handler.ts 完整实现了这套过滤:INTERNAL_SERVER_ERROR时用logger.error记录堆栈(error.stack ?? String(error.cause ?? error))并调用@crm/telemetry的apiError上报(携带还原后的/trpc/${path}与status: 500);其他错误码则降级为logger.warn,只记message/type/path/code,避免噪音淹没真实故障。
errorFormatter:唯一能改写客户端载荷的选项
与onError不同,errorFormatter是挂在 options 对象上的普通函数(不是可注入类),它负责塑形客户端收到的错误载荷——既能附加结构化的校验错误,又能保证内部消息不会在生产环境泄漏出去:
TRPCModule.forRoot({ errorFormatter({ shape, error }) { return { ...shape, data: { ...shape.data, zodError: error.cause instanceof ZodError ? error.cause.flatten() : null, }, }; }, })tRPC 官方的错误格式化规范可参考 tRPC 文档的 error formatting 章节;核心心智模型是:onError看内部、errorFormatter看外部,前者做可观测性,后者做客户端契约。
Comp AI CRM 的 error-formatter.ts 是一个值得精读的生产级范例:它把 tRPC 字符串化后的整个ZodError从“满是code、minimum、inclusive、path的 JSON 数组”提炼成人类可读的完整句子(如email must be a valid email)。两个实现细节值得注意:
- 用duck-typing(鸭子类型)解析 cause 而非
instanceof ZodError——错误跨包边界传递时,workspace 里如果存在两份 zod 副本会导致instanceof静默失效,把原始 JSON 又打回屏幕上。 - 对以标点结尾的 message 直接原样返回(项目自定义的校验文案本身是整句),否则才拼接字段名前缀;空句子与重复句子会被过滤去重。
transformer:JSON 表达不了的类型的救星
数据 transformer 用于传输 JSON 无法直接表达的类型——Date、Map、Set、BigInt、undefined:
import superjson from 'superjson'; TRPCModule.forRoot({ transformer: superjson })三条硬性约束:
- 客户端必须配置同一个 transformer,否则每个请求都会反序列化失败。
- 自 v2.5.0 起 CLI 在生成 schema 时会自动探测 transformer,生成的类型会把 transformer 的影响考虑进去。
- 新增或移除 transformer 是破坏性的线格式变更——必须两端(服务端与所有客户端)一起部署,不能只改一端灰度。
logger:唯一不经过 DI 的选项
logger接受任意实现了LoggerService的实现,替换默认的ConsoleLogger:
import { Module } from '@nestjs/common'; import { TRPCModule } from 'nestjs-trpc'; import { MyLogger } from './my-logger.service'; @Module({ imports: [TRPCModule.forRoot({ logger: new MyLogger() })], }) export class AppModule {}特别注意:这里传的是实例而不是类——它是全部选项中唯一不参与 DI 解析的,因此一个自身需要注入依赖的 logger 只能手动构造,或从 app context 中获取。Pino 与 Winston 的适配器(如nestjs-pino)都能满足该接口。
Comp AI CRM 传入了new ContextLogger()(apps/api/src/trpc/trpc.module.ts),这个自定义 logger 定义在 context-logger.ts,与仓库自建的日志中间件、异常过滤器共用同一套上下文日志体系,保证 tRPC 请求日志与 Nest 其他日志格式统一。
sse:订阅流的保活与超时调优
TRPCSSEOptions完整类型(v2.13.0 新增):
export interface TRPCSSEOptions { /** Enable SSE subscriptions. @default true */ enabled?: boolean; ping?: { /** @default false */ enabled: boolean; /** Interval in milliseconds. @default 1000 */ intervalMs?: number; }; /** Max duration in ms before ending the stream. @default undefined */ maxDurationMs?: number; /** End the request immediately after data is sent. @default false */ emitAndEndImmediately?: boolean; client?: { /** Client reconnects after this much inactivity, in ms. @default undefined */ reconnectAfterInactivityMs?: number; }; }一个生产环境的安全基线:
TRPCModule.forRoot({ sse: { ping: { enabled: true, intervalMs: 30000 }, client: { reconnectAfterInactivityMs: 60000 }, }, })四个关键认知:
- Ping 默认关闭,而
intervalMs默认1000。如果只开 ping 不设间隔,每个打开的流每秒会收到一次 ping——务必显式设置间隔。 - 间隔要低于代理的空闲超时。30s 对 nginx、ALB、Cloudflare 常见的 60s 默认值是安全的。
client.reconnectAfterInactivityMs应明显大于ping.intervalMs,否则客户端会在健康的 ping 间隙里反复重连。emitAndEndImmediately是为无法保持流式响应的 serverless 运行时准备的;在常规长驻 Node 服务器上请保持关闭——它会让订阅失去意义。maxDurationMs用来封顶流生命周期:当上游负载均衡器反正会切断连接时,主动收尾比被动掐断能让客户端重连得更干净。
jsonl:批量流式响应的保活心跳
TRPCJSONLOptions只有一个字段:
export interface TRPCJSONLOptions { /** Interval in ms between keep-alive pings on streamed batch responses. @default undefined */ pingMs?: number; }它作用于httpBatchStreamLink的批量流式响应(不是订阅)。适用场景:你批量请求了多个 procedure,其中某个慢 procedure 让整个响应挂着不返回,超过了代理的空闲超时。此时给批量流加心跳:
TRPCModule.forRoot({ jsonl: { pingMs: 30000 } })已经消失的选项:autoSchemaFile 与 schemaFileImports
autoSchemaFile和schemaFileImports是v1 时代的选项,在 v2 的TRPCModuleOptions类型中不存在——传给forRoot会直接报 TypeScript 错误。但官方多篇文档(context、client、integrations 页面)至今仍展示autoSchemaFile: './src/@generated',这是 v1/v2 混排造成的坑。
v2 中输出位置是 CLI 的职责:nestjs-trpc generate --output ./src/@generated,相关细节见 codegen-and-client.md。
(兼容性备注:v2 CLI 在扫描你的forRoot调用时仍会识别字面量autoSchemaFile,以帮助从 v1 迁移的项目——但不要依赖它,它不属于类型化 API。)
Express 与 Fastify:零配置双平台适配
nestjs-trpc原生同时支持 Express 与 Fastify,无需任何配置:适配器会检查 Nest 的HttpAdapterHost并自动选择正确的 tRPC adapter。
唯一的可见影响在ContextOptions的类型上——它是CreateExpressContextOptions | CreateFastifyContextOptions的联合类型。在伸手去取某个驱动特有的 request 属性之前,先做类型收窄,你的 context 类就能保持跨平台可移植。这正是 Comp AI CRM 的TrpcContext.create中"req" in opts ? opts.req : undefined判断的用意:兼容两个平台的同时保持类型安全。
实战对照:一份完整的生产级 forRoot 配置
把本文所有要点汇聚成 Comp AI CRM 的实际装配(apps/api/src/trpc/trpc.module.ts):
@Module({ imports: [ TRPCModule.forRoot({ basePath: "/api/trpc", context: TrpcContext, logger: new ContextLogger(), errorFormatter: formatTrpcError, onError: TrpcErrorHandler, globalMiddlewares: [LoggingMiddleware, DomainErrorMiddleware], }), ], providers: [ TrpcContext, TrpcErrorHandler, LoggingMiddleware, DomainErrorMiddleware, AuthMiddleware, SessionOnlyMiddleware, ], exports: [AuthMiddleware, SessionOnlyMiddleware], }) export class TrpcModule {}这份配置体现了本文的全部原则:
- basePath 显式声明,避免默认
/trpc与全局前缀组合产生歧义; - context 类实现了完整的会话解析,并独立于模块定义时间;
- 全局中间件只做日志与异常翻译,认证(
AuthMiddleware)与会话限制(SessionOnlyMiddleware)通过exports交给各 router 显式@UseMiddlewares; - onError 按错误码分级:500 级才上报遥测,其余仅 warn;
- errorFormatter 把 Zod 校验错误翻译成可读句子,不泄漏内部堆栈;
- logger 传入项目自定义实例,与全局日志体系打通。
对照 v2.13.0 的类型定义逐项核验后可以发现:这份配置恰好覆盖了TRPCModuleOptions中除transformer、sse、jsonl之外的全部字段——项目暂未引入自定义 transformer(数据均为 JSON 可表达类型),也暂未启用订阅与批量流式传输;一旦启用,可分别按本文的 SSE 保活基线与jsonl: { pingMs: 30000 }方案直接扩展。
参考链接
- apps/api/src/trpc/trpc.module.ts — forRoot 生产装配示例
- apps/api/src/trpc/trpc.context.ts — context 类实现
- apps/api/src/trpc/error-formatter.ts — errorFormatter 生产实现
- apps/api/src/trpc/trpc-error.handler.ts — onError 分级上报实现
- apps/api/src/trpc/middlewares — 全局/认证中间件目录
- middlewares-and-context.md — context 类与中间件链的完整讨论
- codegen-and-client.md — CLI 生成与客户端消费
- api-reference.md — 从发布的 .d.ts 转写的完整导出符号与类型签名
- 后端
- 前端
- CRM
- 人工智能
- AI Agent
【免费下载链接】crm
Comp AI CRM is an open source, CRM designed for AI agents. Agentic-first CRM.
相关推荐
NestJS + tRPC 端到端类型安全实战:nestjs-trpc 适配器完全指南(基于 Comp AI CRM 源码验证)
NestJS + tRPC 端到端类型安全实战:nestjs trpc 适配器完全指南(基于 Comp AI CRM 源码验证) 本文是一份以 nestjs t
后端前端CRM人工智能AI AgentComp AI CRM 实战:nestjs-trpc 中间件与上下文的完整指南
Comp AI CRM 实战:nestjs trpc 中间件与上下文的完整指南 导读 本指南以 nestjs trpc 官方技能文档为骨架,结合 Comp AI
后端前端CRM人工智能AI Agentnestjs-trpc 代码生成与客户端使用:Comp AI CRM 的端到端类型安全实践
nestjs trpc 代码生成与客户端使用:Comp AI CRM 的端到端类型安全实践 本篇技术指南聚焦 nestjs trpc 的代码生成(Codegen
后端前端CRM人工智能AI Agent
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考