Skip to content

TypeScript tRPC 网关 API

基于 tRPC 11 + Express 5 构建的类型安全 API 网关,为前端应用提供统一的端到端类型安全接口层。

API 文档https://halolight-bff.h7ml.cn

GitHubhttps://github.com/halolight/halolight-bff

特性

  • 🎯 端到端类型安全 - tRPC 提供从服务器到客户端的完整类型推导,零运行时开销
  • 🔐 JWT 双令牌认证 - Access Token + Refresh Token 自动续期,RBAC 权限控制
  • 📡 服务网关聚合 - 统一聚合多个后端服务 (Python/Java/Go/Bun),自动故障转移
  • Zod 数据验证 - 所有输入自动验证,类型安全,详细错误信息
  • 🔄 SuperJSON 序列化 - 自动处理 Date、Map、Set、BigInt、RegExp 等复杂类型
  • 🎭 请求批处理 - 自动批量处理多个请求,减少网络开销
  • 📊 分布式追踪 - Trace ID 自动传播,完整请求链路日志
  • 🐳 Docker 支持 - 容器化部署,生产级配置

技术栈

技术版本说明
TypeScript5.9编程语言
tRPC11RPC 框架
Zod-数据验证
Express5Web 服务器
SuperJSON-序列化
JWT-身份认证
Pino-日志系统

快速开始

环境要求

  • Node.js >= 20.0
  • pnpm >= 8.0
  • 至少一个后端服务 (Python/Java/Go/Bun)

安装

bash
# 克隆仓库
git clone https://github.com/halolight/halolight-bff.git
cd halolight-bff

# 安装依赖
pnpm install

环境变量

bash
cp .env.example .env
env
# 服务器配置
PORT=3002
HOST=0.0.0.0
NODE_ENV=development

# JWT 密钥(生产环境必须修改)
JWT_SECRET=your-super-secret-key-at-least-32-characters-long
JWT_ACCESS_EXPIRES=15m
JWT_REFRESH_EXPIRES=7d

# CORS 配置
CORS_ORIGIN=*

# 日志级别
LOG_LEVEL=info

# 后端服务注册(至少配置一个)
HALOLIGHT_API_PYTHON_URL=http://localhost:8000
HALOLIGHT_API_BUN_URL=http://localhost:3000
HALOLIGHT_API_JAVA_URL=http://localhost:8080
HALOLIGHT_API_NESTJS_URL=http://localhost:3001
HALOLIGHT_API_NODE_URL=http://localhost:3003
HALOLIGHT_API_GO_URL=http://localhost:8081

数据库初始化

无需数据库 (API 网关不直接操作数据库)。

启动服务

bash
# 开发模式(热重载)
pnpm dev

# 生产模式
pnpm build
pnpm start

访问 http://localhost:3002

项目结构

halolight-bff/
├── src/
│   ├── index.ts              # 应用入口
│   ├── server.ts             # Express 服务器 + tRPC 适配器
│   ├── trpc.ts               # tRPC 实例和 procedure 定义
│   ├── context.ts            # Context 创建(用户、追踪、服务)
│   ├── routers/
│   │   ├── index.ts          # 根 router(组合所有模块)
│   │   ├── auth.ts           # 认证模块(8 个端点)
│   │   ├── users.ts          # 用户管理(8 个端点)
│   │   ├── dashboard.ts      # 仪表盘统计(9 个端点)
│   │   ├── permissions.ts    # 权限管理(7 个端点)
│   │   ├── roles.ts          # 角色管理(8 个端点)
│   │   ├── teams.ts          # 团队管理(9 个端点)
│   │   ├── folders.ts        # 文件夹管理(8 个端点)
│   │   ├── files.ts          # 文件管理(9 个端点)
│   │   ├── documents.ts      # 文档管理(10 个端点)
│   │   ├── calendar.ts       # 日历事件(10 个端点)
│   │   ├── notifications.ts  # 通知(7 个端点)
│   │   └── messages.ts       # 消息/聊天(9 个端点)
│   ├── middleware/
│   │   └── auth.ts           # JWT 认证/授权中间件
│   ├── services/
│   │   ├── httpClient.ts     # HTTP 客户端(后端通信)
│   │   └── serviceRegistry.ts # 后端服务注册表
│   └── schemas/
│       ├── index.ts          # Schema 导出
│       └── common.ts         # 通用 Zod schemas(分页、排序、响应)
├── .env.example              # 环境变量模板
├── .github/workflows/        # CI/CD 配置
├── Dockerfile                # Docker 镜像构建
├── docker-compose.yml        # Docker Compose 配置
├── package.json              # 依赖配置
└── tsconfig.json             # TypeScript 配置

API 模块

HaloLight BFF 提供 12 个核心业务模块,覆盖 100+ tRPC 端点

模块端点数描述
auth8登录、注册、令牌刷新、登出、密码管理
users8用户 CRUD、角色/状态管理、个人资料
dashboard9统计数据、访问趋势、销售数据、任务、日程
permissions7权限 CRUD、树结构、模块权限、批量操作
roles8角色 CRUD、权限分配、用户关联
teams9团队 CRUD、成员管理、邀请、权限
folders8文件夹 CRUD、树结构、移动、面包屑
files9文件 CRUD、上传、下载、移动、复制、共享
documents10文档 CRUD、版本控制、协作、分享
calendar10事件 CRUD、参与者管理、RSVP、提醒
notifications7通知列表、未读数、标记已读、批量删除
messages9对话管理、消息 CRUD、发送、已读状态

认证相关端点

Procedure类型描述权限
auth.loginmutation用户登录公开
auth.registermutation用户注册公开
auth.refreshmutation刷新令牌公开
auth.logoutmutation退出登录需认证
auth.forgotPasswordmutation忘记密码公开
auth.resetPasswordmutation重置密码公开
auth.verifyEmailmutation验证邮箱公开
auth.changePasswordmutation修改密码需认证

用户管理端点

Procedure类型描述权限
users.listquery获取用户列表users:view
users.byIdquery获取用户详情users:view
users.mequery获取当前用户需认证
users.createmutation创建用户users:create
users.updatemutation更新用户users:update
users.deletemutation删除用户users:delete
users.updateRolemutation更新用户角色users:update
users.updateStatusmutation更新用户状态users:update

完整端点清单

仪表盘 (Dashboard) - 9 个端点

Procedure类型描述
dashboard.getStatsquery统计数据(用户、文档、文件、任务)
dashboard.getVisitsquery访问趋势(7天/30天)
dashboard.getSalesquery销售数据(折线图)
dashboard.getPieDataquery饼图数据(分类占比)
dashboard.getTasksquery待办任务列表
dashboard.getCalendarquery今日日程
dashboard.getActivitiesquery最近活动
dashboard.getNotificationsquery最新通知
dashboard.getProgressquery项目进度

权限管理 (Permissions) - 7 个端点

Procedure类型描述
permissions.listquery获取权限列表
permissions.treequery获取权限树
permissions.byIdquery获取权限详情
permissions.createmutation创建权限
permissions.updatemutation更新权限
permissions.deletemutation删除权限
permissions.modulesquery获取权限模块

角色管理 (Roles) - 8 个端点

Procedure类型描述
roles.listquery获取角色列表
roles.byIdquery获取角色详情
roles.createmutation创建角色
roles.updatemutation更新角色
roles.deletemutation删除角色
roles.assignPermissionsmutation分配权限
roles.removePermissionsmutation移除权限
roles.usersquery获取角色下的用户

团队管理 (Teams) - 9 个端点

Procedure类型描述
teams.listquery获取团队列表
teams.byIdquery获取团队详情
teams.createmutation创建团队
teams.updatemutation更新团队
teams.deletemutation删除团队
teams.addMembermutation添加成员
teams.removeMembermutation移除成员
teams.updateMemberRolemutation更新成员角色
teams.membersquery获取团队成员

文件夹管理 (Folders) - 8 个端点

Procedure类型描述
folders.listquery获取文件夹列表
folders.treequery获取文件夹树
folders.byIdquery获取文件夹详情
folders.createmutation创建文件夹
folders.updatemutation更新文件夹
folders.deletemutation删除文件夹
folders.movemutation移动文件夹
folders.breadcrumbquery获取面包屑路径

文件管理 (Files) - 9 个端点

Procedure类型描述
files.listquery获取文件列表
files.byIdquery获取文件详情
files.uploadmutation上传文件
files.updatemutation更新文件信息
files.deletemutation删除文件
files.movemutation移动文件
files.copymutation复制文件
files.downloadquery获取下载链接
files.sharemutation共享文件

文档管理 (Documents) - 10 个端点

Procedure类型描述
documents.listquery获取文档列表
documents.byIdquery获取文档详情
documents.createmutation创建文档
documents.updatemutation更新文档
documents.deletemutation删除文档
documents.versionsquery获取版本历史
documents.restoremutation恢复版本
documents.sharemutation共享文档
documents.unsharemutation取消共享
documents.collaboratorsquery获取协作者

日历管理 (Calendar) - 10 个端点

Procedure类型描述
calendar.eventsquery获取日程列表
calendar.byIdquery获取日程详情
calendar.createmutation创建日程
calendar.updatemutation更新日程
calendar.deletemutation删除日程
calendar.addAttendeemutation添加参与者
calendar.removeAttendeemutation移除参与者
calendar.rsvpmutationRSVP 响应
calendar.setRemindermutation设置提醒
calendar.byMonthquery按月获取日程

通知管理 (Notifications) - 7 个端点

Procedure类型描述
notifications.listquery获取通知列表
notifications.unreadCountquery获取未读数
notifications.markReadmutation标记已读
notifications.markAllReadmutation全部已读
notifications.deletemutation删除通知
notifications.deleteAllmutation删除全部
notifications.preferencesquery获取通知偏好

消息管理 (Messages) - 9 个端点

Procedure类型描述
messages.conversationsquery获取对话列表
messages.byConversationquery获取对话消息
messages.sendmutation发送消息
messages.markReadmutation标记已读
messages.deletemutation删除消息
messages.createConversationmutation创建对话
messages.deleteConversationmutation删除对话
messages.searchquery搜索消息
messages.unreadCountquery获取未读数

核心概念

tRPC Procedures

tRPC 提供三种 procedure 类型:

typescript
// 公开端点 - 无需认证
export const publicProcedure = t.procedure;

// 受保护端点 - 需要有效 JWT
export const protectedProcedure = t.procedure.use(isAuthenticated);

// 管理员端点 - 需要 admin 角色
export const adminProcedure = t.procedure.use(isAdmin);

使用示例

typescript
export const usersRouter = router({
  // Query - 查询数据
  list: protectedProcedure
    .input(z.object({
      page: z.number().default(1),
      limit: z.number().default(10),
      keyword: z.string().optional(),
    }))
    .query(async ({ input, ctx }) => {
      // ctx.user 包含已认证用户信息
      const client = ctx.services.getDefault();
      const data = await client.get('/api/users', { query: input });
      return { code: 200, message: 'success', data };
    }),

  // Mutation - 修改数据
  create: adminProcedure
    .input(z.object({
      name: z.string().min(2),
      email: z.string().email(),
      role: z.string(),
    }))
    .mutation(async ({ input, ctx }) => {
      const client = ctx.services.getDefault();
      const data = await client.post('/api/users', { body: input });
      return { code: 201, message: 'Created', data };
    }),
});

Context

每个请求都会创建一个独立的 context:

typescript
interface Context {
  req: Request;                    // Express 请求对象
  res: Response;                   // Express 响应对象
  user: JWTPayload | null;         // 已认证用户(通过 JWT)
  traceId: string;                 // 分布式追踪 ID(UUID)
  services: ServiceRegistry;       // 后端服务注册表
}

Context 创建流程

  1. 解析 Authorization 头中的 JWT Token
  2. 验证 Token 有效性,提取用户信息
  3. 生成唯一的 traceId (用于分布式追踪)
  4. 注入 ServiceRegistry (后端服务集合)

JWT Token 结构

typescript
interface JWTPayload {
  id: string;                      // 用户 ID
  name: string;                    // 用户名
  email: string;                   // 邮箱
  role: {
    id: string;                    // 角色 ID
    name: string;                  // 角色名称(如 admin, user)
    label: string;                 // 角色显示名称
    permissions: string[];         // 权限列表(如 ["users:*", "documents:view"])
  };
}

Token 使用

typescript
// 客户端发送请求
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

// 服务端自动解析并注入到 ctx.user
const userId = ctx.user.id;
const userPermissions = ctx.user.role.permissions;

权限系统

支持灵活的通配符权限匹配:

权限格式说明示例
*所有权限(超级管理员)可执行任何操作
{resource}:*模块所有操作users:* = 用户模块所有权限
{resource}:{action}特定操作users:view = 仅查看用户

权限检查示例

typescript
// 在 middleware 中检查权限
export const requirePermission = (permission: string) => {
  return t.middleware(({ ctx, next }) => {
    if (!ctx.user) {
      throw new TRPCError({ code: 'UNAUTHORIZED' });
    }

    const hasPermission = ctx.user.role.permissions.some(p =>
      p === '*' ||
      p === permission ||
      (p.endsWith(':*') && permission.startsWith(p.slice(0, -1)))
    );

    if (!hasPermission) {
      throw new TRPCError({ code: 'FORBIDDEN' });
    }

    return next();
  });
};

// 使用
export const deleteUser = protectedProcedure
  .use(requirePermission('users:delete'))
  .input(z.object({ id: z.string() }))
  .mutation(async ({ input, ctx }) => {
    // 只有拥有 users:delete 权限的用户可以执行
  });

服务注册与发现

通过环境变量配置多个后端服务:

bash
# Python FastAPI
HALOLIGHT_API_PYTHON_URL=http://api-python:8000

# Bun Hono
HALOLIGHT_API_BUN_URL=http://api-bun:3000

# Java Spring Boot
HALOLIGHT_API_JAVA_URL=http://api-java:8080

# Go Fiber
HALOLIGHT_API_GO_URL=http://api-go:8081

服务优先级:按配置顺序,第一个可用的服务作为默认服务。

使用示例

typescript
// 使用默认服务(优先级最高的)
const client = ctx.services.getDefault();
const data = await client.get('/api/users');

// 使用特定服务
const pythonClient = ctx.services.get('python');
const stats = await pythonClient.get('/api/dashboard/stats');

// 故障转移:如果默认服务不可用,自动切换到下一个服务
try {
  const data = await ctx.services.getDefault().get('/api/users');
} catch (error) {
  // ServiceRegistry 自动重试其他服务
}

响应格式

所有 API 遵循统一的响应结构:

typescript
// 标准响应
interface APIResponse<T> {
  code: number;        // HTTP 状态码(200, 201, 400, 500...)
  message: string;     // 人类可读消息(success, error, ...)
  data: T | null;      // 响应数据(成功时)或 null(失败时)
}

// 分页响应
interface PaginatedResponse<T> {
  code: number;
  message: string;
  data: {
    list: T[];         // 数据列表
    total: number;     // 总记录数
    page: number;      // 当前页码
    limit: number;     // 每页条数
    totalPages?: number; // 总页数(可选)
  };
}

示例

typescript
// 成功响应
{
  "code": 200,
  "message": "success",
  "data": {
    "id": "1",
    "name": "John Doe",
    "email": "john@example.com"
  }
}

// 分页响应
{
  "code": 200,
  "message": "success",
  "data": {
    "list": [{ "id": "1", "name": "User 1" }],
    "total": 100,
    "page": 1,
    "limit": 10,
    "totalPages": 10
  }
}

// 错误响应(tRPC 自动格式化)
{
  "error": {
    "code": "UNAUTHORIZED",
    "message": "Not authenticated"
  }
}

认证机制

JWT 双令牌

Access Token:  15 分钟有效期,用于 API 请求
Refresh Token: 7 天有效期,用于刷新 Access Token

请求头

http
Authorization: Bearer <access_token>

刷新流程

typescript
// 客户端示例
const refreshToken = async () => {
  const refreshToken = localStorage.getItem('refreshToken');
  const result = await trpc.auth.refresh.mutate({ refreshToken });

  localStorage.setItem('accessToken', result.data.accessToken);
  localStorage.setItem('refreshToken', result.data.refreshToken);

  return result.data.accessToken;
};

// tRPC 客户端配置 - 自动刷新
const client = createTRPCProxyClient<AppRouter>({
  links: [
    httpBatchLink({
      url: 'http://localhost:3002/trpc',
      async headers() {
        let token = localStorage.getItem('accessToken');

        // 如果 token 过期,自动刷新
        if (isTokenExpired(token)) {
          token = await refreshToken();
        }

        return {
          authorization: `Bearer ${token}`,
        };
      },
    }),
  ],
});

错误处理

tRPC 错误类型

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

// 400 - 请求参数错误
throw new TRPCError({
  code: 'BAD_REQUEST',
  message: 'Invalid input',
});

// 401 - 未认证
throw new TRPCError({
  code: 'UNAUTHORIZED',
  message: 'Not authenticated',
});

// 403 - 无权限
throw new TRPCError({
  code: 'FORBIDDEN',
  message: 'Insufficient permissions',
});

// 404 - 资源不存在
throw new TRPCError({
  code: 'NOT_FOUND',
  message: 'Resource not found',
});

// 409 - 资源冲突
throw new TRPCError({
  code: 'CONFLICT',
  message: 'Email already exists',
});

// 500 - 服务器错误
throw new TRPCError({
  code: 'INTERNAL_SERVER_ERROR',
  message: 'Something went wrong',
});

错误响应格式

json
{
  "error": {
    "code": "UNAUTHORIZED",
    "message": "Not authenticated",
    "data": {
      "code": "UNAUTHORIZED",
      "httpStatus": 401,
      "path": "auth.login"
    }
  }
}

客户端使用

React + @tanstack/react-query

typescript
import { createTRPCReact } from '@trpc/react-query';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { httpBatchLink } from '@trpc/client';
import superjson from 'superjson';
import type { AppRouter } from 'halolight-bff';

// 创建 tRPC React hooks
const trpc = createTRPCReact<AppRouter>();

// 创建 tRPC 客户端
const trpcClient = trpc.createClient({
  transformer: superjson, // 支持 Date、Map、Set 等复杂类型
  links: [
    httpBatchLink({
      url: 'http://localhost:3002/trpc',
      headers() {
        return {
          authorization: `Bearer ${localStorage.getItem('token')}`,
        };
      },
    }),
  ],
});

// 创建 React Query 客户端
const queryClient = new QueryClient();

// 根组件
function App() {
  return (
    <trpc.Provider client={trpcClient} queryClient={queryClient}>
      <QueryClientProvider client={queryClient}>
        <UserList />
      </QueryClientProvider>
    </trpc.Provider>
  );
}

// 使用 tRPC hooks
function UserList() {
  // Query - 自动管理加载状态、缓存、重新获取
  const { data, isLoading, error } = trpc.users.list.useQuery({
    page: 1,
    limit: 10,
  });

  // Mutation - 自动管理加载状态、错误处理
  const createUser = trpc.users.create.useMutation({
    onSuccess: () => {
      // 自动刷新用户列表
      trpc.users.list.invalidate();
    },
  });

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <div>
      <button onClick={() => createUser.mutate({
        name: 'New User',
        email: 'new@example.com',
        role: 'user',
      })}>
        Create User
      </button>

      {data?.data.list.map(user => (
        <div key={user.id}>{user.name}</div>
      ))}
    </div>
  );
}

Next.js App Router

typescript
// app/api/trpc/[trpc]/route.ts - tRPC API 路由
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { appRouter } from '@/server/routers';

const handler = (req: Request) =>
  fetchRequestHandler({
    endpoint: '/api/trpc',
    req,
    router: appRouter,
    createContext: () => ({}),
  });

export { handler as GET, handler as POST };

// app/providers.tsx - tRPC Provider
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { httpBatchLink } from '@trpc/client';
import { createTRPCReact } from '@trpc/react-query';
import superjson from 'superjson';
import type { AppRouter } from '@/server/routers';

const trpc = createTRPCReact<AppRouter>();

export function TRPCProvider({ children }: { children: React.ReactNode }) {
  const [queryClient] = useState(() => new QueryClient());
  const [trpcClient] = useState(() =>
    trpc.createClient({
      transformer: superjson,
      links: [
        httpBatchLink({
          url: '/api/trpc',
        }),
      ],
    })
  );

  return (
    <trpc.Provider client={trpcClient} queryClient={queryClient}>
      <QueryClientProvider client={queryClient}>
        {children}
      </QueryClientProvider>
    </trpc.Provider>
  );
}

// app/page.tsx - Server Component
import { createCaller } from '@/server/routers';

export default async function Page() {
  const caller = createCaller({ req: {}, res: {}, user: null });
  const stats = await caller.dashboard.getStats();

  return <div>Total Users: {stats.data.totalUsers}</div>;
}

Vue 3 + TanStack Query

typescript
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';
import { useQuery, useMutation, useQueryClient } from '@tanstack/vue-query';
import superjson from 'superjson';
import type { AppRouter } from 'halolight-bff';

// 创建 tRPC 客户端
const trpc = createTRPCProxyClient<AppRouter>({
  transformer: superjson,
  links: [
    httpBatchLink({
      url: 'http://localhost:3002/trpc',
      headers() {
        return {
          authorization: `Bearer ${localStorage.getItem('token')}`,
        };
      },
    }),
  ],
});

// 在组件中使用
export default {
  setup() {
    const queryClient = useQueryClient();

    // Query
    const { data, isLoading } = useQuery({
      queryKey: ['users', { page: 1 }],
      queryFn: () => trpc.users.list.query({ page: 1, limit: 10 }),
    });

    // Mutation
    const createUser = useMutation({
      mutationFn: (user) => trpc.users.create.mutate(user),
      onSuccess: () => {
        queryClient.invalidateQueries({ queryKey: ['users'] });
      },
    });

    return { data, isLoading, createUser };
  },
};

Vanilla TypeScript

typescript
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';
import type { AppRouter } from 'halolight-bff';
import superjson from 'superjson';

const client = createTRPCProxyClient<AppRouter>({
  transformer: superjson,
  links: [
    httpBatchLink({
      url: 'http://localhost:3002/trpc',
      headers() {
        return {
          authorization: `Bearer ${localStorage.getItem('token')}`,
        };
      },
    }),
  ],
});

// 使用(完整类型推导)
const users = await client.users.list.query({ page: 1 });
console.log(users.data.list); // TS 自动推导类型

const newUser = await client.users.create.mutate({
  name: 'John',
  email: 'john@example.com',
  role: 'user',
});

开发指南

添加新 Router

  1. 创建新的 router 文件
typescript
// src/routers/products.ts
import { z } from 'zod';
import { router, protectedProcedure, adminProcedure } from '../trpc';

export const productsRouter = router({
  // Query - 获取产品列表
  list: protectedProcedure
    .input(z.object({
      page: z.number().default(1),
      limit: z.number().default(10),
      category: z.string().optional(),
    }))
    .query(async ({ input, ctx }) => {
      const client = ctx.services.getDefault();
      const data = await client.get('/api/products', { query: input });
      return { code: 200, message: 'success', data };
    }),

  // Query - 获取产品详情
  byId: protectedProcedure
    .input(z.object({
      id: z.string(),
    }))
    .query(async ({ input, ctx }) => {
      const client = ctx.services.getDefault();
      const data = await client.get(`/api/products/${input.id}`);
      return { code: 200, message: 'success', data };
    }),

  // Mutation - 创建产品(需要管理员权限)
  create: adminProcedure
    .input(z.object({
      name: z.string().min(2),
      price: z.number().positive(),
      category: z.string(),
    }))
    .mutation(async ({ input, ctx }) => {
      const client = ctx.services.getDefault();
      const data = await client.post('/api/products', { body: input });
      return { code: 201, message: 'Created', data };
    }),

  // Mutation - 更新产品
  update: adminProcedure
    .input(z.object({
      id: z.string(),
      name: z.string().min(2).optional(),
      price: z.number().positive().optional(),
    }))
    .mutation(async ({ input, ctx }) => {
      const { id, ...updateData } = input;
      const client = ctx.services.getDefault();
      const data = await client.put(`/api/products/${id}`, { body: updateData });
      return { code: 200, message: 'Updated', data };
    }),

  // Mutation - 删除产品
  delete: adminProcedure
    .input(z.object({
      id: z.string(),
    }))
    .mutation(async ({ input, ctx }) => {
      const client = ctx.services.getDefault();
      await client.delete(`/api/products/${input.id}`);
      return { code: 200, message: 'Deleted', data: null };
    }),
});
  1. 在根 router 中注册
typescript
// src/routers/index.ts
import { router } from '../trpc';
import { authRouter } from './auth';
import { usersRouter } from './users';
import { productsRouter } from './products'; // 导入新 router

export const appRouter = router({
  auth: authRouter,
  users: usersRouter,
  products: productsRouter, // 注册新 router
  // ... 其他 routers
});

export type AppRouter = typeof appRouter;
  1. 客户端使用
typescript
// 类型自动推导,无需手动定义
const products = await trpc.products.list.query({ page: 1 });
const product = await trpc.products.byId.query({ id: '1' });
const newProduct = await trpc.products.create.mutate({
  name: 'iPhone 15',
  price: 999,
  category: 'electronics',
});

添加自定义 Middleware

typescript
// src/middleware/rateLimit.ts
import { TRPCError } from '@trpc/server';
import { t } from '../trpc';

const rateLimitMap = new Map<string, { count: number; resetAt: number }>();

export const rateLimit = (maxRequests: number, windowMs: number) => {
  return t.middleware(({ ctx, next }) => {
    const key = ctx.user?.id || ctx.req.ip;
    const now = Date.now();

    const record = rateLimitMap.get(key);

    if (!record || now > record.resetAt) {
      rateLimitMap.set(key, { count: 1, resetAt: now + windowMs });
      return next();
    }

    if (record.count >= maxRequests) {
      throw new TRPCError({
        code: 'TOO_MANY_REQUESTS',
        message: 'Rate limit exceeded',
      });
    }

    record.count++;
    return next();
  });
};

// 使用
export const limitedProcedure = protectedProcedure.use(
  rateLimit(10, 60000) // 每分钟最多 10 个请求
);

添加 Schema 验证

typescript
// src/schemas/product.ts
import { z } from 'zod';

export const productSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(2).max(100),
  price: z.number().positive(),
  category: z.enum(['electronics', 'clothing', 'books']),
  stock: z.number().int().nonnegative(),
  createdAt: z.date(),
  updatedAt: z.date(),
});

export const createProductSchema = productSchema.omit({
  id: true,
  createdAt: true,
  updatedAt: true,
});

export const updateProductSchema = createProductSchema.partial();

// 在 router 中使用
export const productsRouter = router({
  create: adminProcedure
    .input(createProductSchema)
    .mutation(async ({ input, ctx }) => {
      // input 已经过 Zod 验证,类型安全
    }),

  update: adminProcedure
    .input(z.object({
      id: z.string(),
      data: updateProductSchema,
    }))
    .mutation(async ({ input, ctx }) => {
      // ...
    }),
});

常用命令

bash
# 开发
pnpm dev              # 启动开发服务器(热重载)
pnpm dev:watch        # 启动开发服务器(文件监听)

# 构建
pnpm build            # 构建生产版本
pnpm start            # 启动生产服务器

# 测试
pnpm test             # 运行测试
pnpm test:watch       # 监听模式运行测试
pnpm test:coverage    # 生成测试覆盖率

# 代码质量
pnpm lint             # 运行 ESLint
pnpm lint:fix         # 自动修复 lint 错误
pnpm type-check       # TypeScript 类型检查
pnpm format           # Prettier 格式化代码

部署

Docker

bash
# 构建镜像
docker build -t halolight-bff .

# 运行容器
docker run -p 3002:3002 \
  -e JWT_SECRET=your-secret-key \
  -e HALOLIGHT_API_PYTHON_URL=http://api-python:8000 \
  halolight-bff

Docker Compose

yaml
# docker-compose.yml
version: '3.8'

services:
  bff:
    build: .
    ports:
      - "3002:3002"
    environment:
      - NODE_ENV=production
      - JWT_SECRET=${JWT_SECRET}
      - HALOLIGHT_API_PYTHON_URL=http://api-python:8000
      - HALOLIGHT_API_BUN_URL=http://api-bun:3000
      - HALOLIGHT_API_JAVA_URL=http://api-java:8080
    depends_on:
      - api-python
      - api-bun
      - api-java
    restart: unless-stopped

  api-python:
    image: halolight-api-python
    ports:
      - "8000:8000"

  api-bun:
    image: halolight-api-bun
    ports:
      - "3000:3000"

  api-java:
    image: halolight-api-java
    ports:
      - "8080:8080"
bash
docker-compose up -d

生产环境配置

env
NODE_ENV=production
PORT=3002
HOST=0.0.0.0

# 强密钥(至少 32 字符)
JWT_SECRET=your-production-secret-key-with-at-least-32-characters
JWT_ACCESS_EXPIRES=15m
JWT_REFRESH_EXPIRES=7d

# 限制 CORS
CORS_ORIGIN=https://your-frontend.com

# 生产日志
LOG_LEVEL=warn

# 后端服务
HALOLIGHT_API_PYTHON_URL=https://api-python.production.com
HALOLIGHT_API_BUN_URL=https://api-bun.production.com
HALOLIGHT_API_JAVA_URL=https://api-java.production.com

性能优化

1。启用请求批处理

tRPC 自动批处理多个并发请求,减少网络开销:

typescript
// 客户端配置
const trpcClient = trpc.createClient({
  links: [
    httpBatchLink({
      url: 'http://localhost:3002/trpc',
      maxURLLength: 2083, // 最大 URL 长度
    }),
  ],
});

// 这三个请求会自动批处理为一个 HTTP 请求
const [users, stats, notifications] = await Promise.all([
  trpc.users.list.query({ page: 1 }),
  trpc.dashboard.getStats.query(),
  trpc.notifications.unreadCount.query(),
]);

2。使用 DataLoader 避免 N+1 查询

typescript
import DataLoader from 'dataloader';

// 创建 DataLoader
const userLoader = new DataLoader(async (ids: string[]) => {
  const users = await db.user.findMany({
    where: { id: { in: ids } },
  });
  return ids.map(id => users.find(u => u.id === id));
});

// 在 context 中注入
export const createContext = (opts: CreateExpressContextOptions) => {
  return {
    ...opts,
    loaders: {
      user: userLoader,
    },
  };
};

// 在 router 中使用
export const postsRouter = router({
  list: protectedProcedure.query(async ({ ctx }) => {
    const posts = await db.post.findMany();

    // 批量加载作者信息,避免 N+1 查询
    const postsWithAuthors = await Promise.all(
      posts.map(async (post) => ({
        ...post,
        author: await ctx.loaders.user.load(post.authorId),
      }))
    );

    return postsWithAuthors;
  }),
});

3。缓存策略

typescript
// 使用 Redis 缓存
import Redis from 'ioredis';

const redis = new Redis();

export const dashboardRouter = router({
  getStats: protectedProcedure.query(async ({ ctx }) => {
    const cacheKey = `dashboard:stats:${ctx.user.id}`;

    // 尝试从缓存获取
    const cached = await redis.get(cacheKey);
    if (cached) {
      return JSON.parse(cached);
    }

    // 从后端服务获取
    const client = ctx.services.getDefault();
    const data = await client.get('/api/dashboard/stats');

    // 缓存 5 分钟
    await redis.setex(cacheKey, 300, JSON.stringify(data));

    return data;
  }),
});

4。限流保护

typescript
import rateLimit from 'express-rate-limit';

// 在 Express 中配置全局限流
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 分钟
  max: 100, // 最多 100 个请求
  message: 'Too many requests from this IP',
});

app.use('/trpc', limiter);

安全建议

1。使用强 JWT 密钥

bash
# 生成强密钥(至少 32 字符)
openssl rand -base64 32

# 在 .env 中配置
JWT_SECRET=your-generated-secret-key-with-at-least-32-characters

2。启用 HTTPS

typescript
// 在生产环境强制使用 HTTPS
if (process.env.NODE_ENV === 'production') {
  app.use((req, res, next) => {
    if (!req.secure) {
      return res.redirect(`https://${req.headers.host}${req.url}`);
    }
    next();
  });
}

3。限制 CORS

bash
# 只允许特定源
CORS_ORIGIN=https://your-frontend.com

# 或多个源(逗号分隔)
CORS_ORIGIN=https://app1.com,https://app2.com

4。输入验证

typescript
// 使用 Zod 严格验证所有输入
export const createUser = protectedProcedure
  .input(z.object({
    name: z.string().min(2).max(50),
    email: z.string().email(),
    age: z.number().int().positive().max(150),
    role: z.enum(['admin', 'user']),
  }))
  .mutation(async ({ input, ctx }) => {
    // input 已经过严格验证
  });

5。日志脱敏

typescript
// 使用 Pino redact 配置
const logger = pino({
  redact: {
    paths: [
      'req.headers.authorization',
      'req.body.password',
      'req.body.token',
      'res.headers["set-cookie"]',
    ],
    remove: true, // 完全移除敏感字段
  },
});

可观测性

日志系统

typescript
// 使用 Pino 结构化日志
import pino from 'pino';
import pinoHttp from 'pino-http';

const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
  transport: {
    target: 'pino-pretty',
    options: {
      colorize: true,
      translateTime: 'SYS:standard',
      ignore: 'pid,hostname',
    },
  },
});

// HTTP 请求日志
app.use(pinoHttp({ logger }));

// 在 router 中使用
export const usersRouter = router({
  create: adminProcedure.mutation(async ({ input, ctx }) => {
    logger.info({ userId: ctx.user.id, input }, 'Creating user');

    try {
      const data = await createUser(input);
      logger.info({ userId: ctx.user.id, data }, 'User created');
      return data;
    } catch (error) {
      logger.error({ userId: ctx.user.id, error }, 'Failed to create user');
      throw error;
    }
  }),
});

健康检查

typescript
// 健康检查端点
app.get('/health', async (req, res) => {
  try {
    // 检查后端服务连接
    const services = await Promise.all([
      fetch(`${process.env.HALOLIGHT_API_PYTHON_URL}/health`),
      fetch(`${process.env.HALOLIGHT_API_BUN_URL}/health`),
    ]);

    const allHealthy = services.every(s => s.ok);

    res.status(allHealthy ? 200 : 503).json({
      status: allHealthy ? 'healthy' : 'unhealthy',
      timestamp: new Date().toISOString(),
      services: {
        python: services[0].ok,
        bun: services[1].ok,
      },
    });
  } catch (error) {
    res.status(503).json({
      status: 'unhealthy',
      error: error.message,
    });
  }
});

监控指标

typescript
// Prometheus 指标
import promClient from 'prom-client';

// 创建注册表
const register = new promClient.Registry();

// 收集默认指标
promClient.collectDefaultMetrics({ register });

// 自定义指标
const httpRequestDuration = new promClient.Histogram({
  name: 'http_request_duration_seconds',
  help: 'Duration of HTTP requests in seconds',
  labelNames: ['method', 'route', 'status_code'],
  registers: [register],
});

// 暴露指标端点
app.get('/metrics', async (req, res) => {
  res.set('Content-Type', register.contentType);
  res.end(await register.metrics());
});

常见问题

Q:端口已被占用

A:修改 .env 中的 PORT 配置,或者终止占用端口的进程:

bash
# 查找占用端口的进程
lsof -i :3002

# 终止进程
kill -9 <PID>

# 或修改端口
echo "PORT=3003" >> .env

Q:CORS 错误

A:更新 .env 中的 CORS_ORIGIN 为允许的源地址:

bash
# 开发环境允许所有源
CORS_ORIGIN=*

# 生产环境指定源
CORS_ORIGIN=https://your-frontend.com

Q:Token 验证失败

A:确保 JWT_SECRET 在所有环境中保持一致:

bash
# 检查 JWT_SECRET 是否一致
echo $JWT_SECRET

# 重新生成 Token
curl -X POST http://localhost:3002/trpc/auth.login \
  -H "Content-Type: application/json" \
  -d '{"email":"admin@example.com","password":"password"}'

Q:后端服务连接失败

A:检查后端服务是否正常运行,以及 URL 配置是否正确:

bash
# 检查服务健康状态
curl http://localhost:8000/health
curl http://localhost:3000/health

# 检查环境变量
echo $HALOLIGHT_API_PYTHON_URL
echo $HALOLIGHT_API_BUN_URL

# 测试连接
curl http://localhost:3002/health

Q:类型推导不工作

A:确保正确导出 AppRouter 类型,并在客户端正确引入:

typescript
// 服务端 - src/routers/index.ts
export const appRouter = router({
  // ... routers
});

export type AppRouter = typeof appRouter; // 必须导出类型

// 客户端 - 确保从正确的路径导入
import type { AppRouter } from 'halolight-bff'; // NPM 包
// 或
import type { AppRouter } from '@/server/routers'; // Monorepo

与其他网关对比

特性tRPC BFFGraphQLREST APIgRPC
类型安全⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
开发体验⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
性能⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
学习曲线⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
生态系统⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
文档⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐

相关链接