fullstack-dev

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Full-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):
  1. Stack: Language/framework for backend and frontend (e.g., Express + React, Django + Vue, Go + HTMX)
  2. Service type: API-only, full-stack monolith, or microservice?
  3. Database: SQL (PostgreSQL, SQLite, MySQL) or NoSQL (MongoDB, Redis)?
  4. Integration: REST, GraphQL, tRPC, or gRPC?
  5. Real-time: Needed? If yes — SSE, WebSocket, or polling?
  6. 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.
在搭建任何内容之前,请让用户明确(或从上下文推断)以下信息:
  1. 技术栈:后端和前端使用的语言/框架(例如:Express + React、Django + Vue、Go + HTMX)
  2. 服务类型:仅API、全栈单体应用还是微服务?
  3. 数据库:SQL(PostgreSQL、SQLite、MySQL)还是NoSQL(MongoDB、Redis)?
  4. 集成方式:REST、GraphQL、tRPC还是gRPC?
  5. 实时功能:是否需要?如果需要——SSE、WebSocket还是轮询?
  6. 认证:是否需要?如果需要——JWT、会话、OAuth还是第三方服务(Clerk、Auth.js)?
如果用户已经在请求中指定了这些信息,可跳过询问直接进行下一步。

Step 1: Architectural Decisions

步骤1:架构决策

Based on requirements, make and state these decisions before coding:
DecisionOptionsReference
Project structureFeature-first (recommended) vs layer-firstSection 1
API client approachTyped fetch / React Query / tRPC / OpenAPI codegenSection 5
Auth strategyJWT + refresh / session / third-partySection 6
Real-time methodPolling / SSE / WebSocketSection 11
Error handlingTyped error hierarchy + global handlerSection 3
Briefly explain each choice (1 sentence per decision).
根据需求,在编码前确定并声明以下决策:
决策项可选方案参考文档
项目结构优先按功能划分(推荐) vs 按技术层划分第1节
API客户端方案类型化fetch / React Query / tRPC / OpenAPI代码生成第5节
认证策略JWT + 刷新令牌 / 会话 / 第三方服务第6节
实时实现方式轮询 / SSE / WebSocket第11节
错误处理类型化错误层级 + 全局处理器第3节
简要解释每个选择(每个决策一句话)。

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:
  1. Build check: Ensure both backend and frontend compile without errors
    bash
    # Backend
    cd server && npm run build
    # Frontend
    cd client && npm run build
  2. 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>
  3. Integration check: Verify frontend can connect to backend (CORS, API base URL, auth flow)
  4. Real-time check (if applicable): Open two browser tabs, verify changes sync
If any check fails, fix the issue before proceeding.
实现完成后,在宣告完成前执行以下检查:
  1. 构建检查:确保后端和前端都能无错误编译
    bash
    # Backend
    cd server && npm run build
    # Frontend
    cd client && npm run build
  2. 启动与冒烟测试:启动服务器,验证关键端点返回预期响应
    bash
    # 启动服务器后测试
    curl http://localhost:3000/health
    curl http://localhost:3000/api/<resource>
  3. 集成检查:验证前端能连接到后端(CORS、API基础URL、认证流程)
  4. 实时功能检查(如适用):打开两个浏览器标签页,验证变更是否同步
如果任何检查失败,修复问题后再继续。

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)
  • .env.example
    committed (no real secrets)
  • 项目采用优先按功能划分的结构搭建
  • 配置集中管理,环境变量在启动时验证(快速失败)
  • 定义类型化错误层级(而非通用
    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 folders1. Project Structure
Manage config + secrets2. Configuration
Handle errors properly3. Error Handling
Write database code4. Database Access Patterns
Set up API client from frontend5. API Client Patterns
Add auth middleware6. Auth & Middleware
Set up logging7. Logging & Observability
Add background jobs8. Background Jobs
Implement caching9. 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 UI12. Cross-Boundary Error Handling
Harden for production13. Production Hardening
Design API endpointsAPI Design
Design database schemaDatabase Schema
Auth flow (JWT, refresh, Next.js SSR, RBAC)references/auth-flow.md
CORS, env vars, environment managementreferences/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.log

1. ✅ 按功能组织代码,而非按技术层
2. ✅ 控制器中绝不包含业务逻辑
3. ✅ 服务中绝不导入HTTP请求/响应类型
4. ✅ 所有配置来自环境变量,启动时验证,快速失败
5. ✅ 每个错误都有类型、会被日志记录,并返回一致格式
6. ✅ 所有输入在边界处验证——绝不信任客户端的任何内容
7. ✅ 带请求ID的结构化JSON日志——绝不使用console.log

1. 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)
LayerResponsibility❌ Never
ControllerParse request, validate, call service, format responseBusiness logic, DB queries
ServiceBusiness rules, orchestration, transaction mgmtHTTP types (req/res), direct DB
RepositoryDatabase queries, external API callsBusiness 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_service
Go:
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_service
Go:
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 missing
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(`缺失必填环境变量: ${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
undefined
typescript
// 基础类(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
undefined

Base (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)
undefined
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}不存在: {id}", "NOT_FOUND", 404)
undefined

Global 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
undefined
bash
undefined

TypeScript (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
undefined
npx prisma migrate dev alembic revision --autogenerate migrate -source file://migrations npx prisma migrate deploy alembic upgrade head migrate -database $DB up
undefined
✅ 架构变更通过迁移实现,绝不使用手动SQL ✅ 迁移必须可回滚 ✅ 生产环境前审查迁移SQL ❌ 绝不手动修改生产环境架构
undefined

N+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 =
(CPU cores × 2) + spindle_count
(start with 10-20). Always set connection timeout. Use PgBouncer for serverless.

池大小 =
(CPU核心数 × 2) + 磁盘数
(初始值设为10-20)。始终设置连接超时。无服务器环境使用PgBouncer。

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 axios
bash
npx openapi-typescript-codegen \
  --input http://localhost:3001/api/openapi.json \
  --output src/generated/api \
  --client axios

Decision: Which API Client?

决策:选择哪种API客户端?

ApproachWhenType SafetyEffort
Typed fetch wrapperSimple apps, small teamsManual typesLow
React Query + fetchReact apps, server stateManual typesMedium
tRPCSame team, TypeScript both sidesAutomaticLow
OpenAPI generatedPublic API, multi-consumerAutomaticMedium
GraphQL codegenGraphQL APIsAutomaticMedium

方案适用场景类型安全性实现成本
类型化fetch包装器简单应用、小型团队手动定义类型
React Query + fetchReact应用、服务端状态管理手动定义类型中等
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

日志级别

LevelWhenProduction?
errorRequires immediate attention✅ Always
warnUnexpected but handled✅ Always
infoNormal operations, audit trail✅ Always
debugDev 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.log

8. 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 TypeSuggested TTL
User profile5-15 min
Product catalog1-5 min
Config / feature flags30-60 sec
SessionMatch 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 automatically
typescript
// 前端
const formData = new FormData();
formData.append('file', file);
formData.append('description', '头像');
const res = await fetch('/api/upload', { method: 'POST', body: formData });
// 注意:不要设置Content-Type头——浏览器会自动设置边界

Decision

决策

MethodFile SizeServer LoadComplexity
Presigned URLAny (recommended > 5MB)None (direct to storage)Medium
Multipart< 10MBHigh (streams through server)Low
Chunked / Resumable> 100MBMediumHigh

方法文件大小服务端负载复杂度
预签名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

决策

MethodDirectionComplexityWhen
PollingClient → ServerLowSimple status checks, < 10 clients
SSEServer → ClientMediumNotifications, feeds, AI streaming
WebSocketBidirectionalHighChat, 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
1Business logic in routes/controllersMove to service layer
2
process.env
scattered everywhere
Centralized typed config
3
console.log
for logging
Structured JSON logger
4Generic
Error('oops')
Typed error hierarchy
5Direct DB calls in controllersRepository pattern
6No input validationValidate at boundary (Zod/Pydantic)
7Catching errors silentlyLog + rethrow or return error
8No health check endpoints
/health
+
/ready
9Hardcoded config/secretsEnvironment variables
10No graceful shutdownHandle SIGTERM properly
11Hardcode API URL in frontendEnvironment variable (
NEXT_PUBLIC_API_URL
)
12Store JWT in localStorageMemory + httpOnly refresh cookie
13Show raw API errors to usersMap to human-readable messages
14Retry 4xx errorsOnly retry 5xx (server failures)
15Skip loading statesSkeleton/spinner while fetching
16Upload large files through API serverPresigned URL → direct to S3
17Poll for real-time dataSSE or WebSocket
18Duplicate types frontend + backendShared types, tRPC, or OpenAPI codegen

#❌ 不要做✅ 正确做法
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
12JWT存储在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.
OrderService
OrderCreationService
+
OrderFulfillmentService
+
OrderQueryService
. Each focused on one workflow.
症状: 单个服务文件超过500行,包含20+方法。
解决方法: 按子领域拆分。
OrderService
OrderCreationService
+
OrderFulfillmentService
+
OrderQueryService
。每个服务专注于一个工作流。

Issue 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-tenancyreferences/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 issuesreferences/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