fullstack-dev
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseFull-Stack Development Practices
全栈开发实践
MANDATORY WORKFLOW — Follow These Steps In Order
强制工作流 — 按以下步骤执行
When this skill is triggered, you MUST follow this workflow before writing any code.
当触发此技能时,在编写任何代码之前必须遵循此工作流。
Step 0: Gather Requirements
步骤0:收集需求
Before scaffolding anything, ask the user to clarify (or infer from context):
- Stack: Language/framework for backend and frontend (e.g., Express + React, Django + Vue, Go + HTMX)
- Service type: API-only, full-stack monolith, or microservice?
- Database: SQL (PostgreSQL, SQLite, MySQL) or NoSQL (MongoDB, Redis)?
- Integration: REST, GraphQL, tRPC, or gRPC?
- Real-time: Needed? If yes — SSE, WebSocket, or polling?
- Auth: Needed? If yes — JWT, session, OAuth, or third-party (Clerk, Auth.js)?
If the user has already specified these in their request, skip asking and proceed.
在搭建任何内容之前,请让用户明确(或从上下文推断)以下信息:
- 技术栈:后端和前端使用的语言/框架(例如: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)?
如果用户已经在请求中指定了这些信息,可跳过询问直接进行下一步。
Step 1: Architectural Decisions
步骤1:架构决策
Based on requirements, make and state these decisions before coding:
| Decision | Options | Reference |
|---|---|---|
| Project structure | Feature-first (recommended) vs layer-first | Section 1 |
| API client approach | Typed fetch / React Query / tRPC / OpenAPI codegen | Section 5 |
| Auth strategy | JWT + refresh / session / third-party | Section 6 |
| Real-time method | Polling / SSE / WebSocket | Section 11 |
| Error handling | Typed error hierarchy + global handler | Section 3 |
Briefly explain each choice (1 sentence per decision).
Step 2: Scaffold with Checklist
步骤2:按清单搭建项目
Use the appropriate checklist below. Ensure ALL checked items are implemented — do not skip any.
使用以下对应清单。确保所有勾选项都已实现——不要跳过任何一项。
Step 3: Implement Following Patterns
步骤3:遵循模式实现
Write code following the patterns in this document. Reference specific sections as you implement each part.
按照本文档中的模式编写代码。实现每个部分时参考具体章节。
Step 4: Test & Verify
步骤4:测试与验证
After implementation, run these checks before claiming completion:
- Build check: Ensure both backend and frontend compile without errors
bash
# Backend cd server && npm run build # Frontend cd client && npm run build - Start & smoke test: Start the server, verify key endpoints return expected responses
bash
# Start server, then test curl http://localhost:3000/health curl http://localhost:3000/api/<resource> - Integration check: Verify frontend can connect to backend (CORS, API base URL, auth flow)
- Real-time check (if applicable): Open two browser tabs, verify changes sync
If any check fails, fix the issue before proceeding.
实现完成后,在宣告完成前执行以下检查:
- 构建检查:确保后端和前端都能无错误编译
bash
# Backend cd server && npm run build # Frontend cd client && npm run build - 启动与冒烟测试:启动服务器,验证关键端点返回预期响应
bash
# 启动服务器后测试 curl http://localhost:3000/health curl http://localhost:3000/api/<resource> - 集成检查:验证前端能连接到后端(CORS、API基础URL、认证流程)
- 实时功能检查(如适用):打开两个浏览器标签页,验证变更是否同步
如果任何检查失败,修复问题后再继续。
Step 5: Handoff Summary
步骤5:交付总结
Provide a brief summary to the user:
- What was built: List of implemented features and endpoints
- How to run: Exact commands to start backend and frontend
- What's missing / next steps: Any deferred items, known limitations, or recommended improvements
- Key files: List the most important files the user should know about
向用户提供简要总结:
- 已构建内容:已实现的功能和端点列表
- 运行方式:启动后端和前端的具体命令
- 缺失内容 / 下一步建议:任何延迟实现的项、已知限制或推荐的改进方向
- 关键文件:用户需要了解的最重要文件列表
Scope
适用范围
USE this skill when:
- Building a full-stack application (backend + frontend)
- Scaffolding a new backend service or API
- Designing service layers and module boundaries
- Implementing database access, caching, or background jobs
- Writing error handling, logging, or configuration management
- Reviewing backend code for architectural issues
- Hardening for production
- Setting up API clients, auth flows, file uploads, or real-time features
NOT for:
- Pure frontend/UI concerns (use your frontend framework's docs)
- Pure database schema design without backend context
使用此技能的场景:
- 构建全栈应用(后端 + 前端)
- 搭建新的后端服务或API
- 设计服务层和模块边界
- 实现数据库访问、缓存或后台任务
- 编写错误处理、日志或配置管理代码
- 审查后端代码的架构问题
- 生产环境加固
- 设置API客户端、认证流程、文件上传或实时功能
不适用场景:
- 纯前端/UI相关工作(请使用前端框架文档)
- 脱离后端上下文的纯数据库设计
Quick Start — New Backend Service Checklist
快速入门 — 新后端服务检查清单
- Project scaffolded with feature-first structure
- Configuration centralized, env vars validated at startup (fail fast)
- Typed error hierarchy defined (not generic )
Error - Global error handler middleware
- Structured JSON logging with request ID propagation
- Database: migrations set up, connection pooling configured
- Input validation on all endpoints (Zod / Pydantic / Go validator)
- Authentication middleware in place
- Health check endpoints (,
/health)/ready - Graceful shutdown handling (SIGTERM)
- CORS configured (explicit origins, not )
* - Security headers (helmet or equivalent)
- committed (no real secrets)
.env.example
- 项目采用优先按功能划分的结构搭建
- 配置集中管理,环境变量在启动时验证(快速失败)
- 定义类型化错误层级(而非通用)
Error - 实现全局错误处理器中间件
- 实现结构化JSON日志,并传播请求ID
- 数据库:设置迁移,配置连接池
- 所有端点实现输入验证(Zod / Pydantic / Go验证器)
- 部署认证中间件
- 实现健康检查端点(、
/health)/ready - 处理优雅关闭(SIGTERM)
- 配置CORS(明确来源,而非)
* - 设置安全头(helmet或同类工具)
- 提交文件(不包含真实密钥)
.env.example
Quick Start — Frontend-Backend Integration Checklist
快速入门 — 前后端集成检查清单
- API client configured (typed fetch wrapper, React Query, tRPC, or OpenAPI generated)
- Base URL from environment variable (not hardcoded)
- Auth token attached to requests automatically (interceptor / middleware)
- Error handling — API errors mapped to user-facing messages
- Loading states handled (skeleton/spinner, not blank screen)
- Type safety across the boundary (shared types, OpenAPI, or tRPC)
- CORS configured with explicit origins (not in production)
* - Refresh token flow implemented (httpOnly cookie + transparent retry on 401)
- 配置API客户端(类型化fetch包装器、React Query、tRPC或OpenAPI生成)
- API基础URL来自环境变量(而非硬编码)
- 认证令牌自动附加到请求(拦截器 / 中间件)
- 错误处理——API错误映射为用户友好的提示信息
- 处理加载状态(骨架屏/加载动画,而非空白页面)
- 跨边界的类型安全(共享类型、OpenAPI或tRPC)
- 配置CORS时使用明确来源(生产环境不使用)
* - 实现刷新令牌流程(httpOnly cookie + 401时自动重试)
Quick Navigation
快速导航
| Need to… | Jump to |
|---|---|
| Organize project folders | 1. Project Structure |
| Manage config + secrets | 2. Configuration |
| Handle errors properly | 3. Error Handling |
| Write database code | 4. Database Access Patterns |
| Set up API client from frontend | 5. API Client Patterns |
| Add auth middleware | 6. Auth & Middleware |
| Set up logging | 7. Logging & Observability |
| Add background jobs | 8. Background Jobs |
| Implement caching | 9. Caching |
| Upload files (presigned URL, multipart) | 10. File Upload Patterns |
| Add real-time features (SSE, WebSocket) | 11. Real-Time Patterns |
| Handle API errors in frontend UI | 12. Cross-Boundary Error Handling |
| Harden for production | 13. Production Hardening |
| Design API endpoints | API Design |
| Design database schema | Database Schema |
| Auth flow (JWT, refresh, Next.js SSR, RBAC) | references/auth-flow.md |
| CORS, env vars, environment management | references/environment-management.md |
| 需要做… | 跳转至 |
|---|---|
| 组织项目文件夹 | 1. 项目结构 |
| 管理配置 + 密钥 | 2. 配置 |
| 正确处理错误 | 3. 错误处理 |
| 编写数据库代码 | 4. 数据库访问模式 |
| 从前端设置API客户端 | 5. API客户端模式 |
| 添加认证中间件 | 6. 认证与中间件 |
| 设置日志 | 7. 日志与可观测性 |
| 添加后台任务 | 8. 后台任务 |
| 实现缓存 | 9. 缓存模式 |
| 文件上传(预签名URL、多部分) | 10. 文件上传模式 |
| 添加实时功能(SSE、WebSocket) | 11. 实时模式 |
| 在前端UI中处理API错误 | 12. 跨边界错误处理 |
| 生产环境加固 | 13. 生产环境加固 |
| 设计API端点 | API设计 |
| 设计数据库 schema | 数据库Schema |
| 认证流程(JWT、刷新令牌、Next.js SSR、RBAC) | references/auth-flow.md |
| CORS、环境变量、环境管理 | references/environment-management.md |
Core Principles (7 Iron Rules)
核心原则(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.log1. ✅ 按功能组织代码,而非按技术层
2. ✅ 控制器中绝不包含业务逻辑
3. ✅ 服务中绝不导入HTTP请求/响应类型
4. ✅ 所有配置来自环境变量,启动时验证,快速失败
5. ✅ 每个错误都有类型、会被日志记录,并返回一致格式
6. ✅ 所有输入在边界处验证——绝不信任客户端的任何内容
7. ✅ 带请求ID的结构化JSON日志——绝不使用console.log1. Project Structure & Layering (CRITICAL)
1. 项目结构与分层(关键)
Feature-First Organization
优先按功能组织
✅ 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/✅ 优先按功能 ❌ 按技术层
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/Three-Layer Architecture
三层架构
Controller (HTTP) → Service (Business Logic) → Repository (Data Access)| Layer | Responsibility | ❌ Never |
|---|---|---|
| Controller | Parse request, validate, call service, format response | Business logic, DB queries |
| Service | Business rules, orchestration, transaction mgmt | HTTP types (req/res), direct DB |
| Repository | Database queries, external API calls | Business logic, HTTP types |
Controller(HTTP层) → Service(业务逻辑层) → Repository(数据访问层)| 层级 | 职责 | ❌ 绝不做 |
|---|---|---|
| Controller | 解析请求、验证、调用服务、格式化响应 | 业务逻辑、数据库查询 |
| Service | 业务规则、编排、事务管理 | HTTP类型(req/res)、直接操作数据库 |
| Repository | 数据库查询、外部API调用 | 业务逻辑、HTTP类型 |
Dependency Injection (All Languages)
依赖注入(全语言支持)
TypeScript:
typescript
class OrderService {
constructor(
private readonly orderRepo: OrderRepository, // ✅ injected interface
private readonly emailService: EmailService,
) {}
}Python:
python
class OrderService:
def __init__(self, order_repo: OrderRepository, email_service: EmailService):
self.order_repo = order_repo # ✅ injected
self.email_service = email_serviceGo:
go
type OrderService struct {
orderRepo OrderRepository // ✅ interface
emailService EmailService
}
func NewOrderService(repo OrderRepository, email EmailService) *OrderService {
return &OrderService{orderRepo: repo, emailService: email}
}TypeScript:
typescript
class OrderService {
constructor(
private readonly orderRepo: OrderRepository, // ✅ 注入接口
private readonly emailService: EmailService,
) {}
}Python:
python
class OrderService:
def __init__(self, order_repo: OrderRepository, email_service: EmailService):
self.order_repo = order_repo # ✅ 注入
self.email_service = email_serviceGo:
go
type OrderService struct {
orderRepo OrderRepository // ✅ 接口
emailService EmailService
}
func NewOrderService(repo OrderRepository, email EmailService) *OrderService {
return &OrderService{orderRepo: repo, emailService: email}
}2. Configuration & Environment (CRITICAL)
2. 配置与环境(关键)
Centralized, Typed, Fail-Fast
集中化、类型化、快速失败
TypeScript:
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:
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 missingTypeScript:
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(`缺失必填环境变量: ${name}`); // 快速失败
return value;
}Python:
python
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
database_url: str # 必填——无此变量应用无法启动
jwt_secret: str # 必填
port: int = 3000 # 可选,带默认值
db_pool_size: int = 10
class Config:
env_file = ".env"
settings = Settings() # 若DATABASE_URL缺失则快速失败Rules
规则
✅ 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✅ 所有配置通过环境变量(十二要素应用原则)
✅ 启动时验证必填变量——快速失败
✅ 在配置层进行类型转换,而非在使用处
✅ 提交带虚拟值的.env.example文件
❌ 绝不硬编码密钥、URL或凭证
❌ 绝不提交.env文件
❌ 绝不将process.env / os.environ分散在代码各处3. Error Handling & Resilience (HIGH)
3. 错误处理与韧性(重要)
Typed Error Hierarchy
类型化错误层级
typescript
// 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);
}
}python
undefinedtypescript
// 基础类(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}不存在: ${id}`, 'NOT_FOUND', 404);
}
}
class ValidationError extends AppError {
constructor(public readonly errors: FieldError[]) {
super('验证失败', 'VALIDATION_ERROR', 422);
}
}python
undefinedBase (Python)
基础类(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)
undefinedclass 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}不存在: {id}", "NOT_FOUND", 404)
undefinedGlobal Error Handler
全局错误处理器
typescript
// 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 });
});typescript
// 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('意外错误', { error: err.message, stack: err.stack, request_id: req.id });
res.status(500).json({ title: '内部错误', status: 500, request_id: req.id });
});Rules
规则
✅ 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')✅ 使用类型化、领域特定的错误类
✅ 全局错误处理器捕获所有错误
✅ 可预期错误 → 结构化响应
✅ 编程错误 → 记录日志 + 通用500响应
✅ 对临时失败使用指数退避重试
❌ 绝不静默捕获并忽略错误
❌ 绝不向客户端返回堆栈跟踪
❌ 绝不抛出通用Error('something')4. Database Access Patterns (HIGH)
4. 数据库访问模式(重要)
Migrations Always
始终使用迁移
bash
undefinedbash
undefinedTypeScript (Prisma) # Python (Alembic) # Go (golang-migrate)
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
undefined✅ Schema changes via migrations, never manual SQL
✅ Migrations must be reversible
✅ Review migration SQL before production
❌ Never modify production schema manually
undefinednpx prisma migrate dev alembic revision --autogenerate migrate -source file://migrations
npx prisma migrate deploy alembic upgrade head migrate -database $DB up
undefined✅ 架构变更通过迁移实现,绝不使用手动SQL
✅ 迁移必须可回滚
✅ 生产环境前审查迁移SQL
❌ 绝不手动修改生产环境架构
undefinedN+1 Prevention
避免N+1查询
typescript
// ❌ 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 } });typescript
// ❌ N+1查询: 1次查询 + N次查询
const orders = await db.order.findMany();
for (const o of orders) { o.items = await db.item.findMany({ where: { orderId: o.id } }); }
// ✅ 单次JOIN查询
const orders = await db.order.findMany({ include: { items: true } });Transactions for Multi-Step Writes
多步骤写入使用事务
typescript
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 });
});typescript
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 });
});Connection Pooling
连接池
Pool size = (start with 10-20). Always set connection timeout. Use PgBouncer for serverless.
(CPU cores × 2) + spindle_count池大小 = (初始值设为10-20)。始终设置连接超时。无服务器环境使用PgBouncer。
(CPU核心数 × 2) + 磁盘数5. API Client Patterns (MEDIUM)
5. API客户端模式(中等)
The "glue layer" between frontend and backend. Choose the approach that fits your team and stack.
前后端之间的“粘合层”。选择适合团队和技术栈的方案。
Option A: Typed Fetch Wrapper (Simple, No Dependencies)
选项A:类型化Fetch包装器(简单,无依赖)
typescript
// 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' }),
};typescript
// 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错误 ${status}`);
}
}
async function api<T>(path: string, options: RequestInit = {}): Promise<T> {
const token = getAuthToken(); // 从cookie / 内存 / 上下文获取
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' }),
};Option B: React Query + Typed Client (Recommended for React)
选项B:React Query + 类型化客户端(React应用推荐)
typescript
// 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} />;
// ...
}typescript
// 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分钟
});
}
export function useCreateOrder() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: CreateOrderInput) =>
apiClient.post<{ data: Order }>('/api/orders', data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['orders'] });
},
});
}
// 组件中使用:
function OrdersPage() {
const { data, isLoading, error } = useOrders();
const createOrder = useCreateOrder();
if (isLoading) return <Skeleton />;
if (error) return <ErrorBanner error={error} />;
// ...
}Option C: tRPC (Same Team Owns Both Sides)
选项C:tRPC(同一团队负责前后端)
typescript
// 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();typescript
// 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: 自动类型安全,无需代码生成
const { data } = trpc.orders.list.useQuery();
const createOrder = trpc.orders.create.useMutation();Option D: OpenAPI Generated Client (Public / Multi-Consumer APIs)
选项D:OpenAPI生成客户端(公开/多消费者API)
bash
npx openapi-typescript-codegen \
--input http://localhost:3001/api/openapi.json \
--output src/generated/api \
--client axiosbash
npx openapi-typescript-codegen \
--input http://localhost:3001/api/openapi.json \
--output src/generated/api \
--client axiosDecision: Which API Client?
决策:选择哪种API客户端?
| Approach | When | Type Safety | Effort |
|---|---|---|---|
| Typed fetch wrapper | Simple apps, small teams | Manual types | Low |
| React Query + fetch | React apps, server state | Manual types | Medium |
| tRPC | Same team, TypeScript both sides | Automatic | Low |
| OpenAPI generated | Public API, multi-consumer | Automatic | Medium |
| GraphQL codegen | GraphQL APIs | Automatic | Medium |
| 方案 | 适用场景 | 类型安全性 | 实现成本 |
|---|---|---|---|
| 类型化fetch包装器 | 简单应用、小型团队 | 手动定义类型 | 低 |
| React Query + fetch | React应用、服务端状态管理 | 手动定义类型 | 中等 |
| tRPC | 同一团队、前后端均为TypeScript | 自动类型安全 | 低 |
| OpenAPI生成 | 公开API、多消费者 | 自动类型安全 | 中等 |
| GraphQL代码生成 | GraphQL API | 自动类型安全 | 中等 |
6. Authentication & Middleware (HIGH)
6. 认证与中间件(重要)
Full reference: references/auth-flow.md — JWT bearer flow, automatic token refresh, Next.js server-side auth, RBAC pattern, backend middleware order.
完整参考: references/auth-flow.md — JWT承载流程、自动令牌刷新、Next.js服务端认证、RBAC模式、后端中间件顺序。
Standard Middleware Order
标准中间件顺序
Request → 1.RequestID → 2.Logging → 3.CORS → 4.RateLimit → 5.BodyParse
→ 6.Auth → 7.Authz → 8.Validation → 9.Handler → 10.ErrorHandler → Response请求 → 1.请求ID → 2.日志 → 3.CORS → 4.限流 → 5.请求体解析
→ 6.认证 → 7.授权 → 8.输入验证 → 9.处理器 → 10.错误处理器 → 响应JWT Rules
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✅ 短过期时间的访问令牌(15分钟) + 服务端存储的刷新令牌
✅ 最小化声明:userId、角色(而非完整用户对象)
✅ 定期轮换签名密钥
❌ 绝不将令牌存储在localStorage(存在XSS风险)
❌ 绝不通过URL查询参数传递令牌RBAC Pattern
RBAC模式
typescript
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);typescript
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);Auth Token Automatic Refresh
认证令牌自动刷新
typescript
// 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;
}
}typescript
// lib/api-client.ts — 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', // 发送httpOnly cookie
});
setAuthToken(refreshed.accessToken);
return api<T>(path, options); // 重试
}
throw err;
}
}7. Logging & Observability (MEDIUM-HIGH)
7. 日志与可观测性(中等-重要)
Structured JSON Logging
结构化JSON日志
typescript
// ✅ 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}`);typescript
// ✅ 结构化日志——可解析、可过滤、可告警
logger.info('订单已创建', {
orderId: order.id, userId: user.id, total: order.total,
items: order.items.length, duration_ms: Date.now() - startTime,
});
// 输出: {"level":"info","msg":"订单已创建","orderId":"ord_123",...}
// ❌ 非结构化日志——大规模场景下无用
console.log(`用户 ${user.id} 创建了订单,总价 ${order.total}`);Log Levels
日志级别
| Level | When | Production? |
|---|---|---|
| error | Requires immediate attention | ✅ Always |
| warn | Unexpected but handled | ✅ Always |
| info | Normal operations, audit trail | ✅ Always |
| debug | Dev troubleshooting | ❌ Dev only |
| 级别 | 适用场景 | 生产环境是否启用 |
|---|---|---|
| error | 需要立即处理的问题 | ✅ 始终启用 |
| warn | 意外但已处理的情况 | ✅ 始终启用 |
| info | 正常操作、审计追踪 | ✅ 始终启用 |
| debug | 开发阶段排查问题 | ❌ 仅开发环境 |
Rules
规则
✅ 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✅ 每条日志都包含请求ID(通过中间件传播)
✅ 在层边界处记录日志(请求进入、响应返回、外部调用)
❌ 绝不记录密码、令牌、PII或密钥
❌ 生产代码中绝不使用console.log8. Background Jobs & Async (MEDIUM)
8. 后台任务与异步处理(中等)
Rules
规则
✅ 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✅ 所有任务必须是幂等的(同一任务执行两次结果相同)
✅ 失败任务 → 重试(最多3次) → 死信队列 → 告警
✅ 工作进程作为独立进程运行(而非API服务器中的线程)
❌ 绝不将长时间运行的任务放在请求处理器中
❌ 绝不假设任务只会执行一次Idempotent Job Pattern
幂等任务模式
typescript
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');
}typescript
async function processPayment(data: { orderId: string }) {
const order = await orderRepo.findById(data.orderId);
if (order.paymentStatus === 'completed') return; // 已处理过,直接返回
await paymentGateway.charge(order);
await orderRepo.updatePaymentStatus(order.id, 'completed');
}9. Caching Patterns (MEDIUM)
9. 缓存模式(中等)
Cache-Aside (Lazy Loading)
缓存旁路(懒加载)
typescript
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;
}typescript
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('用户', id);
await redis.set(`user:${id}`, JSON.stringify(user), 'EX', 900); // 15分钟过期
return user;
}Rules
规则
✅ 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)| Data Type | Suggested TTL |
|---|---|
| User profile | 5-15 min |
| Product catalog | 1-5 min |
| Config / feature flags | 30-60 sec |
| Session | Match session duration |
✅ 始终设置TTL——绝不无过期时间缓存
✅ 写入时失效缓存(更新后删除缓存键)
✅ 缓存仅用于读取,绝不作为权威数据源
❌ 绝不无TTL缓存( stale数据比慢数据更糟)| 数据类型 | 建议TTL |
|---|---|
| 用户资料 | 5-15分钟 |
| 产品目录 | 1-5分钟 |
| 配置 / 功能开关 | 30-60秒 |
| 会话 | 与会话时长一致 |
10. File Upload Patterns (MEDIUM)
10. 文件上传模式(中等)
Option A: Presigned URL (Recommended for Large Files)
选项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)Backend:
typescript
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 });
});Frontend:
typescript
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 });
}客户端 → GET /api/uploads/presign?filename=photo.jpg&type=image/jpeg
服务端 → { uploadUrl: "https://s3.../presigned", fileKey: "uploads/abc123.jpg" }
客户端 → PUT uploadUrl(直接上传到S3,绕过服务端)
客户端 → POST /api/photos { fileKey: "uploads/abc123.jpg" } (保存引用)后端实现:
typescript
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分钟
});
res.json({ uploadUrl: url, fileKey: key });
});前端实现:
typescript
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 });
}Option B: Multipart (Small Files < 10MB)
选项B:多部分上传(小文件 < 10MB)
typescript
// 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 automaticallytypescript
// 前端
const formData = new FormData();
formData.append('file', file);
formData.append('description', '头像');
const res = await fetch('/api/upload', { method: 'POST', body: formData });
// 注意:不要设置Content-Type头——浏览器会自动设置边界Decision
决策
| Method | File Size | Server Load | Complexity |
|---|---|---|---|
| Presigned URL | Any (recommended > 5MB) | None (direct to storage) | Medium |
| Multipart | < 10MB | High (streams through server) | Low |
| Chunked / Resumable | > 100MB | Medium | High |
| 方法 | 文件大小 | 服务端负载 | 复杂度 |
|---|---|---|---|
| 预签名URL | 任意大小(推荐>5MB) | 无(直接上传到存储服务) | 中等 |
| 多部分上传 | <10MB | 高(通过服务端流式传输) | 低 |
| 分片/可恢复上传 | >100MB | 中等 | 高 |
11. Real-Time Patterns (MEDIUM)
11. 实时模式(中等)
Option A: Server-Sent Events (SSE) — One-Way Server → Client
选项A:Server-Sent Events(SSE)——单向服务端→客户端
Best for: notifications, live feeds, streaming AI responses.
Backend (Express):
typescript
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());
});Frontend:
typescript
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]);
}最佳场景:通知、实时信息流、AI响应流式输出。
后端(Express):
typescript
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());
});前端:
typescript
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(() => /* 重连 */, 3000); };
return () => source.close();
}, [userId]);
}Option B: WebSocket — Bidirectional
选项B:WebSocket——双向通信
Best for: chat, collaborative editing, gaming.
Backend (ws library):
typescript
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));
});Frontend:
typescript
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 };
}最佳场景:聊天、协作编辑、游戏。
后端(ws库):
typescript
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, '未授权'); 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', () => { /* 连接活跃 */ });
ws.on('close', () => clearInterval(interval));
});前端:
typescript
function useWebSocket(url: string) {
const [ws, setWs] = useState<WebSocket | null>(null);
useEffect(() => {
const socket = new WebSocket(url);
socket.onopen = () => setWs(socket);
socket.onclose = () => setTimeout(() => /* 重连 */, 3000);
return () => socket.close();
}, [url]);
const send = useCallback((data: unknown) => ws?.send(JSON.stringify(data)), [ws]);
return { ws, send };
}Option C: Polling (Simplest, No Infrastructure)
选项C:轮询(最简单,无需额外基础设施)
typescript
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;
},
});
}typescript
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;
},
});
}Decision
决策
| Method | Direction | Complexity | When |
|---|---|---|---|
| Polling | Client → Server | Low | Simple status checks, < 10 clients |
| SSE | Server → Client | Medium | Notifications, feeds, AI streaming |
| WebSocket | Bidirectional | High | Chat, collaboration, gaming |
| 方法 | 通信方向 | 复杂度 | 适用场景 |
|---|---|---|---|
| 轮询 | 客户端→服务端 | 低 | 简单状态检查、客户端数量<10 |
| SSE | 服务端→客户端 | 中等 | 通知、信息流、AI流式输出 |
| WebSocket | 双向 | 高 | 聊天、协作、游戏 |
12. Cross-Boundary Error Handling (MEDIUM)
12. 跨边界错误处理(中等)
API Error → User-Facing Message
API错误 → 用户友好提示
typescript
// 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.';
}typescript
// lib/error-handler.ts
export function getErrorMessage(error: unknown): string {
if (error instanceof ApiError) {
switch (error.status) {
case 401: return '请登录后继续。';
case 403: return '您没有权限执行此操作。';
case 404: return '您查找的内容不存在。';
case 409: return '此内容与现有数据冲突。';
case 422:
const fields = error.body?.errors;
if (fields?.length) return fields.map((f: any) => f.message).join('。 ');
return '请检查您的输入。';
case 429: return '请求过于频繁,请稍后再试。';
default: return '发生未知错误,请重试。';
}
}
if (error instanceof TypeError && error.message === 'Failed to fetch') {
return '无法连接到服务器,请检查网络连接。';
}
return '发生意外错误。';
}React Query Global Error Handler
React Query全局错误处理器
typescript
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;
},
},
},
});typescript
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;
},
},
},
});Rules
规则
✅ 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)✅ 将每个API错误码映射为人类可读的提示
✅ 字段级验证错误显示在对应表单输入旁
✅ 5xx错误自动重试(最多3次,带退避),4xx错误绝不重试
✅ 刷新令牌失败后401错误重定向到登录页
✅ fetch失败出现TypeError时显示“离线”提示
❌ 绝不向用户显示原始API错误信息(如“NullPointerException”)
❌ 绝不静默忽略错误(显示提示或记录日志)
❌ 绝不重试4xx错误(客户端错误,重试无意义)Integration Decision Tree
集成决策树
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)同一团队负责前后端?
│
├─ 是,均为TypeScript
│ └─ tRPC(端到端类型安全,无需代码生成)
│
├─ 是,不同语言
│ └─ OpenAPI规范 → 生成客户端(通过代码生成实现类型安全)
│
├─ 否,公开API
│ └─ REST + OpenAPI → 为消费者生成SDK
│
└─ 复杂数据需求、多前端
└─ GraphQL + 代码生成(每个客户端可灵活查询)
需要实时功能?
│
├─ 仅服务端→客户端(通知、信息流、AI流式输出)
│ └─ SSE(最简单,自动重连,可通过代理)
│
├─ 双向通信(聊天、协作)
│ └─ WebSocket(需要心跳 + 重连逻辑)
│
└─ 简单状态轮询(客户端数量<10)
└─ React Query refetchInterval(无需额外基础设施)13. Production Hardening (MEDIUM)
13. 生产环境加固(中等)
Health Checks
健康检查
typescript
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 });
});typescript
app.get('/health', (req, res) => res.json({ status: 'ok' })); // 存活检查
app.get('/ready', async (req, res) => { // 就绪检查
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 });
});Graceful Shutdown
优雅关闭
typescript
process.on('SIGTERM', async () => {
logger.info('SIGTERM received');
server.close(); // stop new connections
await drainConnections(); // finish in-flight
await closeDatabase();
process.exit(0);
});typescript
process.on('SIGTERM', async () => {
logger.info('收到SIGTERM信号');
server.close(); // 停止接受新连接
await drainConnections(); // 处理完正在进行的请求
await closeDatabase();
process.exit(0);
});Security Checklist
安全检查清单
✅ 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✅ CORS:明确来源(生产环境绝不使用'*')
✅ 安全头(helmet / 同类工具)
✅ 公开端点限流
✅ 所有端点输入验证(绝不信任任何内容)
✅ 强制HTTPS
❌ 绝不向客户端暴露内部错误Anti-Patterns
反模式
| # | ❌ Don't | ✅ Do Instead |
|---|---|---|
| 1 | Business logic in routes/controllers | Move to service layer |
| 2 | | Centralized typed config |
| 3 | | Structured JSON logger |
| 4 | Generic | Typed error hierarchy |
| 5 | Direct DB calls in controllers | Repository pattern |
| 6 | No input validation | Validate at boundary (Zod/Pydantic) |
| 7 | Catching errors silently | Log + rethrow or return error |
| 8 | No health check endpoints | |
| 9 | Hardcoded config/secrets | Environment variables |
| 10 | No graceful shutdown | Handle SIGTERM properly |
| 11 | Hardcode API URL in frontend | Environment variable ( |
| 12 | Store JWT in localStorage | Memory + httpOnly refresh cookie |
| 13 | Show raw API errors to users | Map to human-readable messages |
| 14 | Retry 4xx errors | Only retry 5xx (server failures) |
| 15 | Skip loading states | Skeleton/spinner while fetching |
| 16 | Upload large files through API server | Presigned URL → direct to S3 |
| 17 | Poll for real-time data | SSE or WebSocket |
| 18 | Duplicate types frontend + backend | Shared types, tRPC, or OpenAPI codegen |
| # | ❌ 不要做 | ✅ 正确做法 |
|---|---|---|
| 1 | 业务逻辑写在路由/控制器中 | 移到服务层 |
| 2 | | 集中化类型化配置 |
| 3 | 使用 | 结构化JSON日志 |
| 4 | 抛出通用 | 类型化错误层级 |
| 5 | 控制器中直接调用数据库 | 仓库模式 |
| 6 | 无输入验证 | 边界处验证(Zod/Pydantic) |
| 7 | 静默捕获错误 | 记录日志 + 重新抛出或返回错误 |
| 8 | 无健康检查端点 | |
| 9 | 硬编码配置/密钥 | 环境变量 |
| 10 | 无优雅关闭 | 正确处理SIGTERM |
| 11 | 前端硬编码API URL | 环境变量( |
| 12 | JWT存储在localStorage | 内存 + httpOnly刷新令牌cookie |
| 13 | 向用户显示原始API错误 | 映射为人类可读提示 |
| 14 | 重试4xx错误 | 仅重试5xx(服务端错误) |
| 15 | 跳过加载状态 | 数据获取时显示骨架屏/加载动画 |
| 16 | 大文件通过API服务器上传 | 预签名URL → 直接上传到S3 |
| 17 | 轮询实现实时数据 | SSE或WebSocket |
| 18 | 前后端重复定义类型 | 共享类型、tRPC或OpenAPI代码生成 |
Common Issues
常见问题
Issue 1: "Where does this business rule go?"
问题1:“这个业务规则应该放在哪里?”
Rule: If it involves HTTP (request parsing, status codes, headers) → controller. If it involves business decisions (pricing, permissions, rules) → service. If it touches the database → repository.
规则: 如果涉及HTTP(请求解析、状态码、头信息)→ 控制器。如果涉及业务决策(定价、权限、规则)→ 服务层。如果操作数据库→ 仓库层。
Issue 2: "Service is getting too big"
问题2:“服务层代码太庞大了”
Symptom: One service file > 500 lines with 20+ methods.
Fix: Split by sub-domain. → + + . Each focused on one workflow.
OrderServiceOrderCreationServiceOrderFulfillmentServiceOrderQueryService症状: 单个服务文件超过500行,包含20+方法。
解决方法: 按子领域拆分。 → + + 。每个服务专注于一个工作流。
OrderServiceOrderCreationServiceOrderFulfillmentServiceOrderQueryServiceIssue 3: "Tests are slow because they hit the database"
问题3:“测试很慢,因为要访问数据库”
Fix: Unit tests mock the repository layer (fast). Integration tests use test containers or transaction rollback (real DB, still fast). Never mock the service layer in integration tests.
解决方法: 单元测试模拟仓库层(快速)。集成测试使用测试容器或事务回滚(真实数据库,仍快速)。集成测试中绝不模拟服务层。
Reference Documents
参考文档
This skill includes deep-dive references for specialized topics. Read the relevant reference when you need detailed guidance.
| Need to… | Reference |
|---|---|
| Write backend tests (unit, integration, e2e, contract, performance) | references/testing-strategy.md |
| Validate a release before deployment (6-gate checklist) | references/release-checklist.md |
| Choose a tech stack (language, framework, database, infra) | references/technology-selection.md |
| Build with Django / DRF (models, views, serializers, admin) | references/django-best-practices.md |
| Design REST/GraphQL/gRPC endpoints (URLs, status codes, pagination) | references/api-design.md |
| Design database schema, indexes, migrations, multi-tenancy | references/db-schema.md |
| Auth flow (JWT bearer, token refresh, Next.js SSR, RBAC, middleware order) | references/auth-flow.md |
| CORS config, env vars per environment, common CORS issues | references/environment-management.md |
此技能包含针对特定主题的深度参考文档。需要详细指导时阅读对应参考文档。
| 需要做… | 参考文档 |
|---|---|
| 编写后端测试(单元、集成、端到端、契约、性能) | 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 |
| 设计数据库schema、索引、迁移、多租户 | references/db-schema.md |
| 认证流程(JWT承载、令牌刷新、Next.js SSR、RBAC、中间件顺序) | references/auth-flow.md |
| CORS配置、各环境环境变量、常见CORS问题 | references/environment-management.md |