首页 > 应用开发 > IT综合 > tRPC类型安全API开发指南:技术面试手册实践解析
# tRPC类型安全API开发指南:技术面试手册实践解析
tRPC作为现代化的类型安全API框架,为全栈TypeScript开发提供了端到端的类型安全保障。本文结合技术面试常见场景,深入探讨其实践应用。
## tRPC基础架构设计
### 1. 核心架构与配置
tRPC采用分层架构,确保类型安全贯穿整个开发流程:
```typescript
// 项目基础结构
/*
client/ # 前端应用
server/ # 后端服务
├── trpc/ # tRPC核心配置
├── routers/ # 路由定义
└── procedures/ # 业务过程
shared/ # 共享类型定义
*/
// trpc/initTRPC.ts - 初始化配置
import { initTRPC, TRPCError } from '@trpc/server';
import { z } from 'zod';
import superjson from 'superjson';
// 创建上下文类型
export interface Context {
user?: {
id: string;
email: string;
role: 'admin' | 'user';
};
req: Request;
res: Response;
}
// 初始化tRPC
const t = initTRPC.context<Context>().create({
transformer: superjson, // 支持Date/Map/Set等类型
errorFormatter({ shape, error }) {
return {
...shape,
data: {
...shape.data,
zodError: error.cause instanceof z.ZodError
? error.cause.flatten()
: null,
},
};
},
});
// 导出基础类型
export const router = t.router;
export const middleware = t.middleware;
export const publicProcedure = t.procedure;
export const mergeRouters = t.mergeRouters;
// 认证中间件
const isAuthenticated = middleware(({ ctx, next }) => {
if (!ctx.user) {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: '请先登录'
});
}
return next({
ctx: {
...ctx,
user: ctx.user, // 类型细化
},
});
});
// 角色检查中间件
const isAdmin = middleware(({ ctx, next }) => {
if (ctx.user?.role !== 'admin') {
throw new TRPCError({
code: 'FORBIDDEN',
message: '需要管理员权限'
});
}
return next();
});
// 创建受保护的过程
export const protectedProcedure = t.procedure.use(isAuthenticated);
export const adminProcedure = protectedProcedure.use(isAdmin);
```
### 2. 路由与过程定义
组织API路由结构,实现业务逻辑:
```typescript
// routers/user.router.ts - 用户相关路由
import { z } from 'zod';
import {
router,
publicProcedure,
protectedProcedure,
adminProcedure
} from '../trpc/initTRPC';
import {
userService,
type UserProfile,
type CreateUserInput
} from '../services/user.service';
// 输入验证模式
const userSchemas = {
create: z.object({
email: z.string().email('邮箱格式不正确'),
password: z.string()
.min(8, '密码至少8位')
.max(32, '密码最多32位')
.regex(
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/,
'密码必须包含大小写字母和数字'
),
name: z.string().min(2, '姓名至少2位').max(50, '姓名最多50位'),
role: z.enum(['admin', 'user']).default('user')
}),
update: z.object({
name: z.string().min(2).max(50).optional(),
avatar: z.string().url().optional(),
bio: z.string().max(500).optional()
}),
pagination: z.object({
page: z.number().int().min(1).default(1),
limit: z.number().int().min(1).max(100).default(20),
search: z.string().optional(),
sortBy: z.enum(['createdAt', 'name', 'email']).default('createdAt'),
order: z.enum(['asc', 'desc']).default('desc')
})
};
export const userRouter = router({
// 公共API:用户注册
register: publicProcedure
.input(userSchemas.create)
.mutation(async ({ input, ctx }) => {
try {
const user = await userService.createUser(input);
// 记录操作日志
await ctx.logService.create({
action: 'USER_REGISTER',
userId: user.id,
metadata: {
email: input.email,
userAgent: ctx.req.headers['user-agent']
}
});
return {
success: true,
data: user,
message: '注册成功'
};
} catch (error) {
if (error instanceof Error) {
throw new TRPCError({
code: 'CONFLICT',
message: error.message,
cause: error
});
}
throw error;
}
}),
// 公共API:用户登录
login: publicProcedure
.input(z.object({
email: z.string().email(),
password: z.string()
}))
.mutation(async ({ input, ctx }) => {
const result = await userService.authenticate(input);
if (!result.success) {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: '邮箱或密码错误'
});
}
// 设置会话
await ctx.sessionService.createSession(
result.user.id,
ctx.req
);
return {
success: true,
data: {
user: result.user,
token: result.token
},
message: '登录成功'
};
}),
// 受保护API:获取当前用户资料
getProfile: protectedProcedure
.query(async ({ ctx }) => {
const user = await userService.findById(ctx.user.id, {
includeSensitive: true
});
if (!user) {
throw new TRPCError({
code: 'NOT_FOUND',
message: '用户不存在'
});
}
return user;
}),
// 受保护API:更新用户资料
updateProfile: protectedProcedure
.input(userSchemas.update)
.mutation(async ({ input, ctx }) => {
const updatedUser = await userService.update(
ctx.user.id,
input
);
return {
success: true,
data: updatedUser,
message: '资料更新成功'
};
}),
// 管理员API:获取用户列表
listUsers: adminProcedure
.input(userSchemas.pagination)
.query(async ({ input, ctx }) => {
const { users, total } = await userService.listUsers(input);
return {
data: users,
pagination: {
page: input.page,
limit: input.limit,
total,
pages: Math.ceil(total / input.limit)
}
};
}),
// 管理员API:删除用户
deleteUser: adminProcedure
.input(z.object({ userId: z.string().uuid() }))
.mutation(async ({ input, ctx }) => {
// 不能删除自己
if (input.userId === ctx.user.id) {
throw new TRPCError({
code: 'BAD_REQUEST',
message: '不能删除自己的账户'
});
}
await userService.deleteUser(input.userId);
return {
success: true,
message: '用户删除成功'
};
})
});
```
## 技术面试实践场景
### 3. 面试问题API实现
模拟技术面试系统的核心功能:
```typescript
// routers/interview.router.ts - 面试系统路由
import { z } from 'zod';
import { router, protectedProcedure } from '../trpc/initTRPC';
import {
questionService,
interviewService,
type Question,
type InterviewSession
} from '../services/interview.service';
export const interviewRouter = router({
// 获取面试问题分类
getCategories: protectedProcedure
.query(async ({ ctx }) => {
const categories = await questionService.getCategories();
return categories.map(category => ({
id: category.id,
name: category.name,
description: category.description,
questionCount: category._count.questions,
difficulty: category.difficulty
}));
}),
// 根据筛选条件获取问题
getQuestions: protectedProcedure
.input(z.object({
categoryId: z.string().optional(),
difficulty: z.enum(['easy', 'medium', 'hard']).optional(),
tags: z.array(z.string()).optional(),
limit: z.number().int().min(1).max(50).default(10),
random: z.boolean().default(true)
}))
.query(async ({ input, ctx }) => {
const questions = await questionService.findQuestions({
where: {
categoryId: input.categoryId,
difficulty: input.difficulty,
tags: input.tags ? {
some: {
name: {
in: input.tags
}
}
} : undefined
},
take: input.limit,
include: {
category: true,
tags: true,
solutions: {
where: {
language: 'typescript'
},
take: 1
}
},
orderBy: input.random
? { id: 'desc' } // 实际应为随机排序
: { createdAt: 'desc' }
});
return questions.map(q => ({
id: q.id,
title: q.title,
description: q.description,
difficulty: q.difficulty,
category: q.category.name,
tags: q.tags.map(t => t.name),
hints: q.hints,
timeLimit: q.timeLimit,
memoryLimit: q.memoryLimit,
template: q.solutions[0]?.code
}));
}),
// 开始新的面试会话
startInterview: protectedProcedure
.input(z.object({
categoryIds: z.array(z.string()),
questionCount: z.number().int().min(1).max(20).default(5),
timeLimit: z.number().int().min(300).max(3600).default(1800) // 30分钟
}))
.mutation(async ({ input, ctx }) => {
const interview = await interviewService.createSession({
userId: ctx.user.id,
categoryIds: input.categoryIds,
questionCount: input.questionCount,
timeLimit: input.timeLimit
});
// 生成面试问题
const questions = await questionService.generateInterviewQuestions({
categoryIds: input.categoryIds,
count: input.questionCount,
userId: ctx.user.id // 用于个性化推荐
});
return {
sessionId: interview.id,
questions: questions.map(q => ({
id: q.id,
title: q.title,
difficulty: q.difficulty,
category: q.category.name
})),
expiresAt: interview.expiresAt,
totalTime: input.timeLimit
};
}),
// 提交问题答案
submitAnswer: protectedProcedure
.input(z.object({
sessionId: z.string().uuid(),
questionId: z.string().uuid(),
code: z.string(),
language: z.enum(['typescript', 'javascript', 'python', 'java']),
explanation: z.string().optional()
}))
.mutation(async ({ input, ctx }) => {
// 验证会话有效性
const session = await interviewService.validateSession(
<"8v.6w5t7.cn"><"2m.9v7k2.cn"><"5w.3p1w2.cn">
input.sessionId,
ctx.user.id
);
if (!session) {
throw new TRPCError({
code: 'NOT_FOUND',
message: '面试会话不存在或已过期'
});
}
// 执行代码测试
const testResult = await interviewService.runTests({
questionId: input.questionId,
code: input.code,
language: input.language
});
// 保存答案
const submission = await interviewService.saveSubmission({
sessionId: input.sessionId,
questionId: input.questionId,
userId: ctx.user.id,
code: input.code,
language: input.language,
explanation: input.explanation,
testResults: testResult,
passed: testResult.passed
});
return {
submissionId: submission.id,
passed: testResult.passed,
testCases: testResult.testCases.map(tc => ({
name: tc.name,
passed: tc.passed,
actual: tc.actual,
expected: tc.expected
})),
executionTime: testResult.executionTime,
memoryUsage: testResult.memoryUsage
};
}),
// 获取面试结果
getInterviewResults: protectedProcedure
.input(z.object({
sessionId: z.string().uuid()
}))
.query(async ({ input, ctx }) => {
const results = await interviewService.getResults(
input.sessionId,
ctx.user.id
);
return {
session: {
id: results.session.id,
startedAt: results.session.startedAt,
completedAt: results.session.completedAt,
totalQuestions: results.session.totalQuestions,
completedQuestions: results.completedQuestions
},
submissions: results.submissions.map(s => ({
questionId: s.questionId,
questionTitle: s.question.title,
difficulty: s.question.difficulty,
submittedAt: s.submittedAt,
passed: s.passed,
executionTime: s.executionTime,
codeQuality: s.codeQualityScore
})),
overallScore: results.overallScore,
strengths: results.strengths,
areasForImprovement: results.areasForImprovement,
recommendations: results.recommendations
};
})
});
```
### 4. 实时协作面试功能
实现面试官与候选人的实时交互:
```typescript
// routers/liveInterview.router.ts - 实时面试路由
import { z } from 'zod';
import { EventEmitter } from 'events';
import { router, protectedProcedure } from '../trpc/initTRPC';
import { observable } from '@trpc/server/observable';
// 实时事件类型
type LiveEvent = {
type: 'CODE_CHANGE' | 'CURSOR_MOVE' | 'CHAT_MESSAGE' | 'TEST_RESULT';
roomId: string;
senderId: string;
timestamp: number;
payload: any;
};
// 共享的事件发射 器
const interviewEmitter = new EventEmitter();
export const liveInterviewRouter = router({
// 加入面试房间
joinRoom: protectedProcedure
.input(z.object({
roomId: z.string(),
role: z.enum(['interviewer', 'candidate'])
}))
.mutation(async ({ input, ctx }) => {
<"9x.6e1w7.cn"><"3c.5w7x1.cn"><"6y.9v1n2.cn"><"0h.2n8j1.cn">
const room = await interviewService.getRoom(input.roomId);
if (!room) {
throw new TRPCError({
code: 'NOT_FOUND',
message: '面试房间不存在'
});
}
// 验证权限
if (input.role === 'interviewer' &&
room.interviewerId !== ctx.user.id) {
throw new TRPCError({
code: 'FORBIDDEN',
message: '没有权限作为面试官加入'
});
}
// 更新房间状态
await interviewService.updateRoomPresence(
input.roomId,
ctx.user.id,
'joined'
);
// 广播加入事件
interviewEmitter.emit('room_event', {
type: 'USER_JOINED',
roomId: input.roomId,
userId: ctx.user.id,
role: input.role,
timestamp: Date.now()
});
return {
success: true,
room: {
id: room.id,
title: room.title,
participants: room.participants,
question: room.currentQuestion,
startedAt: room.startedAt
}
};
}),
// 实时事件订阅
onRoomEvent: protectedProcedure
.input(z.object({ roomId: z.string() }))
.subscription(({ input }) => {
return observable<LiveEvent>((emit) => {
const handler = (event: LiveEvent) => {
if (event.roomId === input.roomId) {
emit.next(event);
}
};
interviewEmitter.on('room_event', handler);
return () => {
interviewEmitter.off('room_event', handler);
};
});
}),
// 发送代码变更
sendCodeChange: protectedProcedure
.input(z.object({
roomId: z.string(),
code: z.string(),
cursorPosition: z.object({
line: z.number(),
column: z.number()
}).optional()
}))
.mutation(async ({ input, ctx }) => {
const event: LiveEvent = {
type: 'CODE_CHANGE',
roomId: input.roomId,
senderId: ctx.user.id,
timestamp: Date.now(),
payload: {
code: input.code,
cursorPosition: input.cursorPosition
}
};
interviewEmitter.emit('room_event', event);
// 保存代码快照
await interviewService.saveCodeSnapshot({
roomId: input.roomId,
userId: ctx.user.id,
code: input.code,
cursorPosition: input.cursorPosition
});
return { success: true };
}),
// 发送聊天消息
sendChatMessage: protectedProcedure
.input(z.object({
roomId: z.string(),
message: z.string().max(1000),
type: z.enum(['question', 'hint', 'feedback']).default('question')
}))
.mutation(async ({ input, ctx }) => {
const event: LiveEvent = {
type: 'CHAT_MESSAGE',
roomId: input.roomId,
senderId: ctx.user.id,
timestamp: Date.now(),
payload: {
message: input.message,
type: input.type,
senderName: ctx.user.name
}
};
interviewEmitter.emit('room_event', event);
// 保存聊天记录
await interviewService.saveChatMessage({
roomId: input.roomId,
userId: ctx.user.id,
message: input.message,
type: input.type
});
return { success: true };
}),
// 运行测试并实时返回结果
runTests: protectedProcedure
.input(z.object({
roomId: z.string(),
code: z.string(),
questionId: z.string().uuid()
}))
.mutation(async ({ input, ctx }) => {
// 开始测试
const testId = await interviewService.startTestRun({
code: input.code,
questionId: input.questionId,
userId: ctx.user.id
});
// 实时发送测试进度
const progressCallback = (progress: any) => {
const event: LiveEvent = {
type: 'TEST_RESULT',
roomId: input.roomId,
senderId: 'system',
timestamp: Date.now(),
payload: {
type: 'PROGRESS',
testId,
progress
}
};
interviewEmitter.emit('room_event', event);
};
// 执行测试
const results = await interviewService.executeTests({
code: input.code,
questionId: input.questionId,
onProgress: progressCallback
});
// 发送最终结果
const finalEvent: LiveEvent = {
type: 'TEST_RESULT',
roomId: input.roomId,
senderId: 'system',
timestamp: Date.now(),
payload: {
type: 'COMPLETE',
testId,
results: results.map(r => ({
name: r.name,
passed: r.passed,
executionTime: r.executionTime
}))
}
};
interviewEmitter.emit('room_event', finalEvent);
return {
testId,
total: results.length,
passed: results.filter(r => r.passed).length,
executionTime: results.reduce((sum, r) => sum + r.executionTime, 0)
};
})
});
```
## 客户端集成与类型安全
### 5. React客户端集成
在React应用中安全地使用tRPC:
```typescript
// client/trpc/client.ts - 客户端配置
import { createTRPCReact } from '@trpc/react-query';
import { httpBatchLink, loggerLink } from '@trpc/client';
import type { AppRouter } from '../../server/routers/_app';
import superjson from 'superjson';
// 创建tRPC React客户端
export const trpc = createTRPCReact<AppRouter>();
// 创建tRPC客户端配置
export const createClient = () => {
return trpc.createClient({
transformer: superjson,
links: [
loggerLink({
enabled: (opts) =>
process.env.NODE_ENV === 'development' ||
(opts.direction === 'down' && opts.result instanceof Error)
}),
httpBatchLink({
url: '/api/trpc',
headers: () => {
const token = localStorage.getItem('token');
return token ? {
Authorization: `Bearer ${token}`
} : {};
},
// 错误处理
fetch: async (input, init) => {
const response = await fetch(input, init);
if (response.status === 401) {
// 处理未授权
window.location.href = '/login';
throw new Error('未授权,请重新登录');
}
return response;
}
})
]
});
};
// client/components/InterviewSession.tsx - 面试组件
import React, { useState } from 'react';
import { trpc } from '../trpc/client';
const InterviewSession: React.FC = () => {
const [sessionId, setSessionId] = useState<string>('');
const [currentQuestion, setCurrentQuestion] = useState(0);
// 使用tRPC查询 - 类型安全!
const { data: session, isLoading } = trpc.interview.startInterview.useQuery(
{
categoryIds: ['algorithms', 'system-design'],
questionCount: 5,
timeLimit: 1800
},
{
onSuccess: (data) => {
setSessionId(data.sessionId);
},
refetchOnWindowFocus: false
}
);
// 提交答案的Mutation
const submitAnswer = trpc.interview.submitAnswer.useMutation({
onSuccess: (data) => {
console.log('答案提交成功:', data);
},
onError: (error) => {
console.error('提交失败:', error.message);
}
});
// 实时订阅
trpc.liveInterview.onRoomEvent.useSubscription(
{ roomId: sessionId },
{
onData: (event) => {
switch (event.type) {
case 'CODE_CHANGE':
// 处理代码变更
break;
case 'CHAT_MESSAGE':
// 显示聊天消息
break;
case 'TEST_RESULT':
// 更新测试结果
break;
}
},
enabled: !!sessionId
}
);
const handleSubmitCode = async (code: string) => {
if (!session || !session.questions[currentQuestion]) return;
await submitAnswer.mutateAsync({
sessionId: session.sessionId,
questionId: session.questions[currentQuestion].id,
code,
language: 'typescript',
explanation: '我的解题思路...'
});
};
if (isLoading) return <div>加载中...</div>;
if (!session) return <div>会话创建失败</div>;
return (
<div className="interview-container">
<h2>技术面试</h2>
<div className="timer">
剩余时间: {Math.floor(session.totalTime / 60)}分钟
</div>
<div className="question-progress">
问题 {currentQuestion + 1} / {session.questions.length}
</div>
{session.questions.map((question, index) => (
<QuestionCard
key={question.id}
question={question}
isActive={index === currentQuestion}
>
<"7z.1f6d1.cn"><"1a.7e5s1.cn">
/>
))}
</div>
);
};
// 自动生成的类型安全Hook
interface QuestionCardProps {
question: {
id: string;
title: string;
difficulty: 'easy' | 'medium' | 'hard';
category: string;
};
isActive: boolean;
onSubmitCode: (code: string) => void;
}
const QuestionCard: React.FC<QuestionCardProps> = ({
question,
isActive,
onSubmitCode
}) => {
// 获取问题详情 - 类型安全!
const { data: questionDetail } = trpc.interview.getQuestions.useQuery({
categoryId: question.category,
difficulty: question.difficulty,
limit: 1
}, {
enabled: isActive
});
const [code, setCode] = useState('');
return (
<div className={`question-card ${isActive ? 'active' : ''}`}>
<h3>{question.title}</h3>
<div className="difficulty-badge">{question.difficulty}</div>
{questionDetail && questionDetail[0] && (
<>
<div className="description">
{questionDetail[0].description}
</div>
<div className="hints">
{questionDetail[0].hints.map((hint, i) => (
<div key={i} className="hint">{hint}</div>
))}
</div>
</>
)}
<textarea
value={code}
=> setCode(e.target.value)}
placeholder="在此处输入你的代码..."
/>
<button => onSubmitCode(code)}>
提交代码
</button>
</div>
);
};
```
### 6. 测试与质量保障
确保API的可靠性和类型安全:
```typescript
// tests/interview.test.ts - 测试用例
import { describe, it, expect, beforeEach } from 'vitest';
import { createCaller } from '../server/trpc/router';
import { createTestContext } from './test-utils';
describe('面试系统API测试', () => {
let caller: ReturnType<typeof createCaller>;
let testContext: any;
beforeEach(async () => {
testContext = await createTestContext();
caller = createCaller(testContext);
});
describe('用户认证', () => {
it('应该成功注册新用户', async () => {
const result = await caller.user.register({
email: 'test@example.com',
password: 'Password123',
name: '测试用户'
});
expect(result.success).toBe(true);
expect(result.data).toHaveProperty('id');
expect(result.data.email).toBe('test@example.com');
});
it('应该拒绝无效的邮箱格式', async () => {
await expect(
caller.user.register({
email: 'invalid-email',
password: 'Password123',
name: '测试用户'
})
).rejects.toThrow('邮箱格式不正确');
});
it('应该成功登录并返回令牌', async () => {
// 先注册
await caller.user.register({
email: 'login@example.com',
password: 'Password123',
name: '登录测试'
});
// 再登录
const result = await caller.user.login({
email: 'login@example.com',
password: 'Password123'
});
expect(result.success).toBe(true);
expect(result.data).toHaveProperty('token');
expect(result.data.user.email).toBe('login@example.com');
});
});
describe('面试流程', () => {
it('应该能创建面试会话', async () => {
const result = await caller.interview.startInterview({
categoryIds: ['algorithms'],
questionCount: 3,
timeLimit: 900
});
expect(result).toHaveProperty('sessionId');
expect(result.questions).toHaveLength(3);
expect(result.totalTime).toBe(900);
});
it('应该验证会话过期时间', async () => {
const result = await caller.interview.startInterview({
categoryIds: ['algorithms'],
questionCount: 3,
timeLimit: 300 // 5分钟
});
const expiresAt = new Date(result.expiresAt);
const now = new Date();
const timeDiff = (expiresAt.getTime() - now.getTime()) / 1000;
// 允许10秒误差
expect(Math.abs(timeDiff - 300)).toBeLessThan(10);
});
it('应该正确提交和评估代码', async () => {
const session = await caller.interview.startInterview({
categoryIds: ['algorithms'],
questionCount: 1
});
const result = await caller.interview.submitAnswer({
sessionId: session.sessionId,
questionId: session.questions[0].id,
code: 'function sum(a, b) { return a + b; }',
language: 'javascript'
});
expect(result).toHaveProperty('submissionId');
expect(result).toHaveProperty('testCases');
expect(Array.isArray(result.testCases)).toBe(true);
});
});
describe('实时协作', () => {
it('应该允许加入面试房间', async () => {
const room = await caller.liveInterview.joinRoom({
roomId: 'test-room-123',
role: 'candidate'
});
expect(room.success).toBe(true);
expect(room.room).toHaveProperty('id');
expect(room.room.id).toBe('test-room-123');
});
it('应该验证面试官权限', async () => {
await expect(
caller.liveInterview.joinRoom({
roomId: 'admin-room',
role: 'interviewer'
})
).rejects.toThrow('没有权限作为面试官加入');
});
});
});
```
tRPC通过端到端的类型安全,为技术面试系统等复杂应用提供了坚实的开发基础。结合Zod进行运行时验证、利用React Query进行状态管理、实现实时订阅功能,可以构建出既安全又高效的全栈应用。在实际开发中,建议采用模块化的路由设计、完善的错误处理机制,并充分利用TypeScript的类型推导能力,确保代码质量和开发效率。
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/19545/viewspace-3105911/ ,如需转载,请注明出处,否则将追究法律责任。