关于 fullstack-dev
fullstack-dev 是面向全栈应用开发与后端服务设计的编码技能。它通过需求确认、架构决策、实施检查清单和交付验证,引导开发过程遵循分层与生产实践。
主要功能
- 01先梳理技术栈与服务需求
- 02指导功能优先的项目结构
- 03提供后端与前后端集成清单
- 04涵盖错误、数据库与认证模式
- 05要求构建、冒烟及集成验证
适用场景
- 01从零搭建全栈应用
- 02规划后端服务模块边界
- 03检查前后端 API 集成
- 04为生产环境加固服务
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ minimax-ai/ skills --skill fullstack-dev安装后在 Claude Code 中输入/fullstack-dev确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/api-design.mdauth-flow.mddb-schema.mddjango-best-practices.mdenvironment-management.mdrelease-checklist.mdtechnology-selection.mdtesting-strategy.md详细介绍
全栈开发实践
强制工作流 — 按顺序完成以下步骤
触发此技能时,必须先遵循此工作流,再编写任何代码。
步骤 0:收集需求
在搭建任何内容之前,请向用户确认(或根据上下文推断):
- 技术栈:后端和前端使用的语言/框架(例如,Express + React、Django + Vue、Go + HTMX)
- 服务类型:仅 API、全栈单体应用,还是微服务?
- 数据库:SQL(PostgreSQL、SQLite、MySQL)还是 NoSQL(MongoDB、Redis)?
- 集成方式:REST、GraphQL、tRPC,还是 gRPC?
- 实时功能:是否需要?如果需要 — 使用 SSE、WebSocket,还是轮询?
- 身份验证:是否需要?如果需要 — 使用 JWT、会话、OAuth,还是第三方服务(Clerk、Auth.js)?
如果用户已在请求中说明这些信息,请跳过提问并继续。
步骤 1:架构决策
根据需求,在编写代码之前,做出并说明以下决策:
| 决策 | 选项 | 参考 |
|---|---|---|
| 项目结构 | 功能优先(推荐)或分层优先 | 第 1 节 |
| API 客户端方案 | 类型化 fetch / React Query / tRPC / OpenAPI 代码生成 | 第 5 节 |
| 身份验证策略 | JWT + 刷新 / 会话 / 第三方 | 第 6 节 |
| 实时通信方式 | 轮询 / SSE / WebSocket | 第 11 节 |
| 错误处理 | 类型化错误层级结构 + 全局处理器 | 第 3 节 |
简要说明每项选择(每项决策用 1 句话)。
步骤 2:按照清单搭建
使用下方适用的清单。确保已实现所有勾选项 — 不要跳过任何一项。
步骤 3:按照模式实现
遵循本文档中的模式编写代码。实现每个部分时,请参考相应章节。
步骤 4:测试与验证
实现完成后,在宣告完成之前运行以下检查:
- 构建检查:确保后端和前端均可无错误编译
# Backend cd server && npm run build # Frontend cd client && npm run build - 启动与冒烟测试:启动服务,确认关键端点返回预期响应
# Start server, then test curl http://localhost:3000/health curl http://localhost:3000/api/<resource> - 集成检查:确认前端可连接后端(CORS、API 基础 URL、身份验证流程)
- 实时功能检查(如适用):打开两个浏览器标签页,确认更改能够同步
如果任何检查失败,请先修复问题再继续。
步骤 5:交付摘要
向用户提供简要摘要:
- 已构建内容:列出已实现的功能和端点
- 运行方式:启动后端和前端的确切命令
- 缺失内容/后续步骤:任何延期事项、已知限制或建议的改进
- 关键文件:列出用户需要了解的最重要文件
适用范围
在以下情况下使用此技能:
- 构建全栈应用(后端 + 前端)
- 搭建新的后端服务或 API
- 设计服务层和模块边界
- 实现数据库访问、缓存或后台任务
- 编写错误处理、日志记录或配置管理
- 审查后端代码中的架构问题
- 为生产环境强化系统
- 设置 API 客户端、身份验证流程、文件上传或实时功能
不适用于:
- 纯前端/UI 问题(请参阅前端框架文档)
- 不涉及后端的纯数据库架构设计
快速入门 — 新建后端服务检查清单
- 项目已搭建,采用功能优先结构
- 配置已集中管理,环境变量已在启动时验证(快速失败)
- 已定义类型化错误层级(而非通用
Error) - 已配置全局错误处理器中间件
- 使用结构化 JSON 日志,并传递请求 ID
- 数据库:已设置迁移,已配置连接池
- 所有端点均已进行输入验证(Zod / Pydantic / Go validator)
- 已配置身份验证中间件
- 已配置健康检查端点(
/health、/ready) - 已处理优雅关闭(SIGTERM)
- 已配置 CORS(明确指定来源,而非
*) - 已配置安全响应头(helmet 或同类工具)
- 已提交
.env.example(不含真实密钥)
快速入门 — 前后端集成检查清单
- 已配置 API 客户端(类型化 fetch 封装、React Query、tRPC 或由 OpenAPI 生成)
- 基础 URL 来自环境变量(未硬编码)
- 请求会自动附加身份验证令牌(拦截器 / 中间件)
- 错误处理 — 将 API 错误映射为面向用户的消息
- 已处理加载状态(骨架屏 / 加载动画,而非空白屏幕)
- 跨边界保持类型安全(共享类型、OpenAPI 或 tRPC)
- 已配置 CORS,并明确指定来源(生产环境中不使用
*) - 已实现刷新令牌流程(httpOnly Cookie + 收到 401 时透明重试)
快速导航
| 需要…… | 跳转至 |
|---|---|
| 组织项目文件夹 | 1. 项目结构 |
| 管理配置和密钥 | 2. 配置 |
| 正确处理错误 | 3. 错误处理 |
| 编写数据库代码 | 4. 数据库访问模式 |
| 从前端设置 API 客户端 | 5. API 客户端模式 |
| 添加身份验证中间件 | 6. 身份验证与中间件 |
| 设置日志记录 | 7. 日志记录与可观测性 |
| 添加后台任务 | 8. 后台任务 |
| 实现缓存 | 9. 缓存 |
| 上传文件(预签名 URL、分段上传) | 10. 文件上传模式 |
| 添加实时功能(SSE、WebSocket) | 11. 实时模式 |
| 在前端 UI 中处理 API 错误 | 12. 跨边界错误处理 |
| 加固生产环境 | 13. 生产环境加固 |
| 设计 API 端点 | API 设计 |
| 设计数据库架构 | 数据库架构 |
| 身份验证流程(JWT、刷新、Next.js SSR、RBAC) | references/auth-flow.md |
| CORS、环境变量、环境管理 | references/environment-management.md |
核心原则(7 条铁律)
1. ✅ Organize by FEATURE, not by technical layer
2. ✅ Controllers never contain business logic
3. ✅ Services never import HTTP request/response types
4. ✅ All config from env vars, validated at startup, fail fast
5. ✅ Every error is typed, logged, and returns consistent format
6. ✅ All input validated at the boundary — trust nothing from client
7. ✅ Structured JSON logging with request ID — not console.log
1. 项目结构与分层(关键)
功能优先的组织方式
✅ Feature-first ❌ Layer-first
src/ src/
orders/ controllers/
order.controller.ts order.controller.ts
order.service.ts user.controller.ts
order.repository.ts services/
order.dto.ts order.service.ts
order.test.ts user.service.ts
users/ repositories/
user.controller.ts ...
user.service.ts
shared/
database/
middleware/
三层架构
Controller (HTTP) → Service (Business Logic) → Repository (Data Access)
| 层 | 职责 | ❌ 绝不包含 |
|---|---|---|
| 控制器 | 解析请求、验证、调用服务、格式化响应 | 业务逻辑、数据库查询 |
| 服务 | 业务规则、流程编排、事务管理 | HTTP 类型(req/res)、直接访问数据库 |
| 仓储 | 数据库查询、外部 API 调用 | 业务逻辑、HTTP 类型 |
依赖注入(所有语言)
TypeScript:
class OrderService {
constructor(
private readonly orderRepo: OrderRepository, // ✅ injected interface
private readonly emailService: EmailService,
) {}
}
Python:
class OrderService:
def __init__(self, order_repo: OrderRepository, email_service: EmailService):
self.order_repo = order_repo # ✅ injected
self.email_service = email_service
Go:
type OrderService struct {
orderRepo OrderRepository // ✅ interface
emailService EmailService
}
func NewOrderService(repo OrderRepository, email EmailService) *OrderService {
return &OrderService{orderRepo: repo, emailService: email}
}
2. 配置与环境(关键)
集中管理、类型化、快速失败
TypeScript:
const config = {
port: parseInt(process.env.PORT || '3000', 10),
database: { url: requiredEnv('DATABASE_URL'), poolSize: intEnv('DB_POOL_SIZE', 10) },
auth: { jwtSecret: requiredEnv('JWT_SECRET'), expiresIn: process.env.JWT_EXPIRES_IN || '1h' },
} as const;
function requiredEnv(name: string): string {
const value = process.env[name];
if (!value) throw new Error(`Missing required env var: ${name}`); // fail fast
return value;
}
Python:
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
database_url: str # required — app won't start without it
jwt_secret: str # required
port: int = 3000 # optional with default
db_pool_size: int = 10
class Config:
env_file = ".env"
settings = Settings() # fails fast if DATABASE_URL missing
规则
✅ All config via environment variables (Twelve-Factor)
✅ Validate required vars at startup — fail fast
✅ Type-cast at config layer, not at usage sites
✅ Commit .env.example with dummy values
❌ Never hardcode secrets, URLs, or credentials
❌ Never commit .env files
❌ Never scatter process.env / os.environ throughout code
3. 错误处理与弹性(高优先级)
类型化错误层级
// Base (TypeScript)
class AppError extends Error {
constructor(
message: string,
public readonly code: string,
public readonly statusCode: number,
public readonly isOperational: boolean = true,
) { super(message); }
}
class NotFoundError extends AppError {
constructor(resource: string, id: string) {
super(`${resource} not found: ${id}`, 'NOT_FOUND', 404);
}
}
class ValidationError extends AppError {
constructor(public readonly errors: FieldError[]) {
super('Validation failed', 'VALIDATION_ERROR', 422);
}
}
# Base (Python)
class AppError(Exception):
def __init__(self, message: str, code: str, status_code: int):
self.message, self.code, self.status_code = message, code, status_code
class NotFoundError(AppError):
def __init__(self, resource: str, id: str):
super().__init__(f"{resource} not found: {id}", "NOT_FOUND", 404)
全局错误处理
// TypeScript (Express)
app.use((err, req, res, next) => {
if (err instanceof AppError && err.isOperational) {
return res.status(err.statusCode).json({
title: err.code, status: err.statusCode,
detail: err.message, request_id: req.id,
});
}
logger.error('Unexpected error', { error: err.message, stack: err.stack, request_id: req.id });
res.status(500).json({ title: 'Internal Error', status: 500, request_id: req.id });
});
规则
✅ Typed, domain-specific error classes
✅ Global error handler catches everything
✅ Operational errors → structured response
✅ Programming errors → log + generic 500
✅ Retry transient failures with exponential backoff
❌ Never catch and ignore errors silently
❌ Never return stack traces to client
❌ Never throw generic Error('something')
4. 数据库访问模式(高)
始终使用迁移
# TypeScript (Prisma) # Python (Alembic) # Go (golang-migrate)
npx prisma migrate dev alembic revision --autogenerate migrate -source file://migrations
npx prisma migrate deploy alembic upgrade head migrate -database $DB up
✅ Schema changes via migrations, never manual SQL
✅ Migrations must be reversible
✅ Review migration SQL before production
❌ Never modify production schema manually
防止 N+1 查询
// ❌ N+1: 1 query + N queries
const orders = await db.order.findMany();
for (const o of orders) { o.items = await db.item.findMany({ where: { orderId: o.id } }); }
// ✅ Single JOIN query
const orders = await db.order.findMany({ include: { items: true } });
多步骤写入使用事务
await db.$transaction(async (tx) => {
const order = await tx.order.create({ data: orderData });
await tx.inventory.decrement({ productId, quantity });
await tx.payment.create({ orderId: order.id, amount });
});
连接池
池大小 = (CPU 核心数 × 2) + 磁盘数(从 10-20 开始)。始终设置连接超时。无服务器场景使用 PgBouncer。
5. API 客户端模式(中)
这是前端与后端之间的“胶合层”。选择适合团队和技术栈的方案。
方案 A:类型化 Fetch 封装(简单,无依赖)
// lib/api-client.ts
const BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
class ApiError extends Error {
constructor(public status: number, public body: any) {
super(body?.detail || body?.message || `API error ${status}`);
}
}
async function api<T>(path: string, options: RequestInit = {}): Promise<T> {
const token = getAuthToken(); // from cookie / memory / context
const res = await fetch(`${BASE_URL}${path}`, {
...options,
headers: {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
...options.headers,
},
});
if (!res.ok) {
const body = await res.json().catch(() => null);
throw new ApiError(res.status, body);
}
if (res.status === 204) return undefined as T;
return res.json();
}
export const apiClient = {
get: <T>(path: string) => api<T>(path),
post: <T>(path: string, data: unknown) => api<T>(path, { method: 'POST', body: JSON.stringify(data) }),
put: <T>(path: string, data: unknown) => api<T>(path, { method: 'PUT', body: JSON.stringify(data) }),
patch: <T>(path: string, data: unknown) => api<T>(path, { method: 'PATCH', body: JSON.stringify(data) }),
delete: <T>(path: string) => api<T>(path, { method: 'DELETE' }),
};
方案 B:React Query + 类型化客户端(推荐用于 React)
// hooks/use-orders.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { apiClient } from '@/lib/api-client';
interface Order { id: string; total: number; status: string; }
interface CreateOrderInput { items: { productId: string; quantity: number }[] }
export function useOrders() {
return useQuery({
queryKey: ['orders'],
queryFn: () => apiClient.get<{ data: Order[] }>('/api/orders'),
staleTime: 1000 * 60, // 1 min
});
}
export function useCreateOrder() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: CreateOrderInput) =>
apiClient.post<{ data: Order }>('/api/orders', data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['orders'] });
},
});
}
// Usage in component:
function OrdersPage() {
const { data, isLoading, error } = useOrders();
const createOrder = useCreateOrder();
if (isLoading) return <Skeleton />;
if (error) return <ErrorBanner error={error} />;
// ...
}
方案 C:tRPC(同一团队负责两端)
// server: trpc/router.ts
export const appRouter = router({
orders: router({
list: publicProcedure.query(async () => {
return db.order.findMany({ include: { items: true } });
}),
create: protectedProcedure
.input(z.object({ items: z.array(orderItemSchema) }))
.mutation(async ({ input, ctx }) => {
return orderService.create(ctx.user.id, input);
}),
}),
});
export type AppRouter = typeof appRouter;
// client: automatic type safety, no code generation
const { data } = trpc.orders.list.useQuery();
const createOrder = trpc.orders.create.useMutation();
方案 D:OpenAPI 生成的客户端(公共 API / 多消费者 API)
npx openapi-typescript-codegen \
--input http://localhost:3001/api/openapi.json \
--output src/generated/api \
--client axios
决策:选择哪种 API 客户端?
| 方案 | 适用场景 | 类型安全 | 工作量 |
|---|---|---|---|
| 类型化 Fetch 封装 | 简单应用、小型团队 | 手动类型 | 低 |
| React Query + Fetch | React 应用、服务端状态 | 手动类型 | 中 |
| tRPC | 同一团队、两端均使用 TypeScript | 自动 | 低 |
| OpenAPI 生成 | 公共 API、多消费者 | 自动 | 中 |
| GraphQL codegen | GraphQL API | 自动 | 中 |
6. 身份验证与中间件(高)
完整参考: references/auth-flow.md — JWT bearer 流程、自动刷新令牌、Next.js 服务端身份验证、RBAC 模式、后端中间件顺序。
标准中间件顺序
Request → 1.RequestID → 2.Logging → 3.CORS → 4.RateLimit → 5.BodyParse
→ 6.Auth → 7.Authz → 8.Validation → 9.Handler → 10.ErrorHandler → Response
JWT 规则
✅ Short expiry access token (15min) + refresh token (server-stored)
✅ Minimal claims: userId, roles (not entire user object)
✅ Rotate signing keys periodically
❌ Never store tokens in localStorage (XSS risk)
❌ Never pass tokens in URL query params
RBAC 模式
function authorize(...roles: Role[]) {
return (req, res, next) => {
if (!req.user) throw new UnauthorizedError();
if (!roles.some(r => req.user.roles.includes(r))) throw new ForbiddenError();
next();
};
}
router.delete('/users/:id', authenticate, authorize('admin'), deleteUser);
身份验证令牌自动刷新
// lib/api-client.ts — transparent refresh on 401
async function apiWithRefresh<T>(path: string, options: RequestInit = {}): Promise<T> {
try {
return await api<T>(path, options);
} catch (err) {
if (err instanceof ApiError && err.status === 401) {
const refreshed = await api<{ accessToken: string }>('/api/auth/refresh', {
method: 'POST',
credentials: 'include', // send httpOnly cookie
});
setAuthToken(refreshed.accessToken);
return api<T>(path, options); // retry
}
throw err;
}
}
7. 日志记录与可观测性(中高)
结构化 JSON 日志
// ✅ Structured — parseable, filterable, alertable
logger.info('Order created', {
orderId: order.id, userId: user.id, total: order.total,
items: order.items.length, duration_ms: Date.now() - startTime,
});
// Output: {"level":"info","msg":"Order created","orderId":"ord_123",...}
// ❌ Unstructured — useless at scale
console.log(`Order created for user ${user.id} with total ${order.total}`);
日志级别
| 级别 | 适用场景 | 生产环境? |
|---|---|---|
| error | 需要立即关注 | ✅ 始终 |
| warn | 意料之外但已处理 | ✅ 始终 |
| info | 正常操作、审计跟踪 | ✅ 始终 |
| debug | 开发调试 | ❌ 仅开发环境 |
规则
✅ Request ID in every log entry (propagated via middleware)
✅ Log at layer boundaries (request in, response out, external call)
❌ Never log passwords, tokens, PII, or secrets
❌ Never use console.log in production code
8. 后台任务与异步处理(中)
规则
✅ All jobs must be IDEMPOTENT (same job running twice = same result)
✅ Failed jobs → retry (max 3) → dead letter queue → alert
✅ Workers run as SEPARATE processes (not threads in API server)
❌ Never put long-running tasks in request handlers
❌ Never assume job runs exactly once
幂等任务模式
async function processPayment(data: { orderId: string }) {
const order = await orderRepo.findById(data.orderId);
if (order.paymentStatus === 'completed') return; // already processed
await paymentGateway.charge(order);
await orderRepo.updatePaymentStatus(order.id, 'completed');
}
9. 缓存模式(中)
Cache-Aside(延迟加载)
async function getUser(id: string): Promise<User> {
const cached = await redis.get(`user:${id}`);
if (cached) return JSON.parse(cached);
const user = await userRepo.findById(id);
if (!user) throw new NotFoundError('User', id);
await redis.set(`user:${id}`, JSON.stringify(user), 'EX', 900); // 15min TTL
return user;
}
规则
✅ ALWAYS set TTL — never cache without expiry
✅ Invalidate on write (delete cache key after update)
✅ Use cache for reads, never for authoritative state
❌ Never cache without TTL (stale data is worse than slow data)
| 数据类型 | 建议 TTL |
|---|---|
| 用户资料 | 5-15 分钟 |
| 产品目录 | 1-5 分钟 |
| 配置 / 功能标志 | 30-60 秒 |
| 会话 | 与会话时长一致 |
10. 文件上传模式(中)
方案 A:预签名 URL(大文件推荐)
Client → GET /api/uploads/presign?filename=photo.jpg&type=image/jpeg
Server → { uploadUrl: "https://s3.../presigned", fileKey: "uploads/abc123.jpg" }
Client → PUT uploadUrl (direct to S3, bypasses your server)
Client → POST /api/photos { fileKey: "uploads/abc123.jpg" } (save reference)
后端:
app.get('/api/uploads/presign', authenticate, async (req, res) => {
const { filename, type } = req.query;
const key = `uploads/${crypto.randomUUID()}-${filename}`;
const url = await s3.getSignedUrl('putObject', {
Bucket: process.env.S3_BUCKET, Key: key,
ContentType: type, Expires: 300, // 5 min
});
res.json({ uploadUrl: url, fileKey: key });
});
前端:
async function uploadFile(file: File) {
const { uploadUrl, fileKey } = await apiClient.get<PresignResponse>(
`/api/uploads/presign?filename=${file.name}&type=${file.type}`
);
await fetch(uploadUrl, { method: 'PUT', body: file, headers: { 'Content-Type': file.type } });
return apiClient.post('/api/photos', { fileKey });
}
方案 B:Multipart(小于 10MB 的文件)
// Frontend
const formData = new FormData();
formData.append('file', file);
formData.append('description', 'Profile photo');
const res = await fetch('/api/upload', { method: 'POST', body: formData });
// Note: do NOT set Content-Type header — browser sets boundary automatically
决策
| 方法 | 文件大小 | 服务器负载 | 复杂度 |
|---|---|---|---|
| 预签名 URL | 任意(推荐大于 5MB) | 无(直接传到存储) | 中 |
| Multipart | 小于 10MB | 高(经由服务器传输) | 低 |
| 分块 / 可恢复上传 | 大于 100MB | 中 | 高 |
11. 实时模式(中)
方案 A:Server-Sent Events(SSE)— 单向:服务器 → 客户端
适用于:通知、实时动态、流式 AI 响应。
后端(Express):
app.get('/api/events', authenticate, (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
});
const send = (event: string, data: unknown) => {
res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
};
const unsubscribe = eventBus.subscribe(req.user.id, (event) => {
send(event.type, event.payload);
});
req.on('close', () => unsubscribe());
});
前端:
function useServerEvents(userId: string) {
useEffect(() => {
const source = new EventSource(`/api/events?userId=${userId}`);
source.addEventListener('notification', (e) => {
showToast(JSON.parse(e.data).message);
});
source.onerror = () => { source.close(); setTimeout(() => /* reconnect */, 3000); };
return () => source.close();
}, [userId]);
}
方案 B:WebSocket — 双向
适用于:聊天、协同编辑、游戏。
后端(ws 库):
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ server: httpServer, path: '/ws' });
wss.on('connection', (ws, req) => {
const userId = authenticateWs(req);
if (!userId) { ws.close(4001, 'Unauthorized'); return; }
ws.on('message', (raw) => handleMessage(userId, JSON.parse(raw.toString())));
ws.on('close', () => cleanupUser(userId));
const interval = setInterval(() => ws.ping(), 30000);
ws.on('pong', () => { /* alive */ });
ws.on('close', () => clearInterval(interval));
});
前端:
function useWebSocket(url: string) {
const [ws, setWs] = useState<WebSocket | null>(null);
useEffect(() => {
const socket = new WebSocket(url);
socket.onopen = () => setWs(socket);
socket.onclose = () => setTimeout(() => /* reconnect */, 3000);
return () => socket.close();
}, [url]);
const send = useCallback((data: unknown) => ws?.send(JSON.stringify(data)), [ws]);
return { ws, send };
}
方案 C:轮询(最简单,无需基础设施)
function useOrderStatus(orderId: string) {
return useQuery({
queryKey: ['order-status', orderId],
queryFn: () => apiClient.get<Order>(`/api/orders/${orderId}`),
refetchInterval: (query) => {
if (query.state.data?.status === 'completed') return false;
return 5000;
},
});
}
决策
| 方法 | 方向 | 复杂度 | 适用场景 |
|---|---|---|---|
| 轮询 | 客户端 → 服务器 | 低 | 简单状态检查,少于 10 个客户端 |
| SSE | 服务器 → 客户端 | 中 | 通知、动态、AI 流式响应 |
| WebSocket | 双向 | 高 | 聊天、协作、游戏 |
12. 跨边界错误处理(中)
API 错误 → 面向用户的消息
// lib/error-handler.ts
export function getErrorMessage(error: unknown): string {
if (error instanceof ApiError) {
switch (error.status) {
case 401: return 'Please log in to continue.';
case 403: return 'You don\'t have permission to do this.';
case 404: return 'The item you\'re looking for doesn\'t exist.';
case 409: return 'This conflicts with an existing item.';
case 422:
const fields = error.body?.errors;
if (fields?.length) return fields.map((f: any) => f.message).join('. ');
return 'Please check your input.';
case 429: return 'Too many requests. Please wait a moment.';
default: return 'Something went wrong. Please try again.';
}
}
if (error instanceof TypeError && error.message === 'Failed to fetch') {
return 'Cannot connect to server. Check your internet connection.';
}
return 'An unexpected error occurred.';
}
React Query 全局错误处理器
const queryClient = new QueryClient({
defaultOptions: {
mutations: { onError: (error) => toast.error(getErrorMessage(error)) },
queries: {
retry: (failureCount, error) => {
if (error instanceof ApiError && error.status < 500) return false;
return failureCount < 3;
},
},
},
});
规则
✅ Map every API error code to a human-readable message
✅ Show field-level validation errors next to form inputs
✅ Auto-retry on 5xx (max 3, with backoff), never on 4xx
✅ Redirect to login on 401 (after refresh attempt fails)
✅ Show "offline" banner when fetch fails with TypeError
❌ Never show raw API error messages to users ("NullPointerException")
❌ Never silently swallow errors (show toast or log)
❌ Never retry 4xx errors (client is wrong, retrying won't help)
集成决策树
Same team owns frontend + backend?
│
├─ YES, both TypeScript
│ └─ tRPC (end-to-end type safety, zero codegen)
│
├─ YES, different languages
│ └─ OpenAPI spec → generated client (type safety via codegen)
│
├─ NO, public API
│ └─ REST + OpenAPI → generated SDKs for consumers
│
└─ Complex data needs, multiple frontends
└─ GraphQL + codegen (flexible queries per client)
Real-time needed?
│
├─ Server → Client only (notifications, feeds, AI streaming)
│ └─ SSE (simplest, auto-reconnect, works through proxies)
│
├─ Bidirectional (chat, collaboration)
│ └─ WebSocket (need heartbeat + reconnection logic)
│
└─ Simple status polling (< 10 clients)
└─ React Query refetchInterval (no infrastructure needed)
13. 生产环境加固(中)
健康检查
app.get('/health', (req, res) => res.json({ status: 'ok' })); // liveness
app.get('/ready', async (req, res) => { // readiness
const checks = {
database: await checkDb(), redis: await checkRedis(),
};
const ok = Object.values(checks).every(c => c.status === 'ok');
res.status(ok ? 200 : 503).json({ status: ok ? 'ok' : 'degraded', checks });
});
优雅关闭
process.on('SIGTERM', async () => {
logger.info('SIGTERM received');
server.close(); // stop new connections
await drainConnections(); // finish in-flight
await closeDatabase();
process.exit(0);
});
安全检查清单
✅ CORS: explicit origins (never '*' in production)
✅ Security headers (helmet / equivalent)
✅ Rate limiting on public endpoints
✅ Input validation on ALL endpoints (trust nothing)
✅ HTTPS enforced
❌ Never expose internal errors to clients
反模式
| # | ❌ 不要 | ✅ 改为 |
|---|---|---|
| 1 | 在路由/控制器中编写业务逻辑 | 移至服务层 |
| 2 | 到处散落 process.env | 集中管理类型化配置 |
| 3 | 使用 console.log 记录日志 | 使用结构化 JSON 日志记录器 |
| 4 | 使用通用的 Error('oops') | 使用类型化错误层级 |
| 5 | 在控制器中直接调用数据库 | 使用仓储模式 |
| 6 | 不验证输入 | 在边界处验证(Zod/Pydantic) |
| 7 | 静默捕获错误 | 记录日志并重新抛出,或返回错误 |
| 8 | 没有健康检查端点 | 添加 /health + /ready |
| 9 | 硬编码配置/密钥 | 使用环境变量 |
| 10 | 未实现优雅关闭 | 正确处理 SIGTERM |
| 11 | 在前端硬编码 API URL | 使用环境变量(NEXT_PUBLIC_API_URL) |
| 12 | 将 JWT 存储在 localStorage 中 | 存储在内存中,并使用 httpOnly 刷新 Cookie |
| 13 | 向用户显示原始 API 错误 | 映射为易于理解的消息 |
| 14 | 重试 4xx 错误 | 仅重试 5xx(服务器故障) |
| 15 | 跳过加载状态 | 获取数据时显示骨架屏/加载指示器 |
| 16 | 通过 API 服务器上传大文件 | 使用预签名 URL → 直接上传到 S3 |
| 17 | 轮询实时数据 | 使用 SSE 或 WebSocket |
| 18 | 在前端和后端重复定义类型 | 使用共享类型、tRPC 或 OpenAPI 代码生成 |
常见问题
问题 1:“业务规则应该放在哪里?”
**规则:**如果涉及 HTTP(请求解析、状态码、标头)→ 放在控制器中。如果涉及业务决策(定价、权限、规则)→ 放在服务中。如果涉及数据库 → 放在仓储中。
问题 2:“服务越来越庞大”
**现象:**单个服务文件超过 500 行,且包含 20 多个方法。
**解决方法:**按子领域拆分。OrderService → OrderCreationService + OrderFulfillmentService + OrderQueryService。每个服务专注于一个工作流。
问题 3:“测试会访问数据库,因此运行很慢”
**解决方法:**单元测试模拟仓储层(速度快)。集成测试使用测试容器或事务回滚(使用真实数据库,但仍然很快)。集成测试中切勿模拟服务层。
参考文档
此技能包含针对专业主题的深入参考资料。需要详细指导时,请阅读相关参考文档。
| 需要… | 参考文档 |
|---|---|
| 编写后端测试(单元、集成、端到端、契约、性能) | references/testing-strategy.md |
| 部署前验证发布内容(6 道关卡检查清单) | references/release-checklist.md |
| 选择技术栈(语言、框架、数据库、基础设施) | references/technology-selection.md |
| 使用 Django / DRF 构建(模型、视图、序列化器、管理后台) | references/django-best-practices.md |
| 设计 REST/GraphQL/gRPC 端点(URL、状态码、分页) | references/api-design.md |
| 设计数据库架构、索引、迁移、多租户 | references/db-schema.md |
| 认证流程(JWT bearer、令牌刷新、Next.js SSR、RBAC、中间件顺序) | references/auth-flow.md |
| 配置 CORS、设置各环境的环境变量、排查常见 CORS 问题 | references/environment-management.md |