mcpField
F
fullstack-dev

fullstack-dev

github.com/minimax-ai开发13.7k3.4k 次安装28 周前更新
代码仓库

关于 fullstack-dev

fullstack-dev 是面向全栈应用开发与后端服务设计的编码技能。它通过需求确认、架构决策、实施检查清单和交付验证,引导开发过程遵循分层与生产实践。

主要功能

  1. 01先梳理技术栈与服务需求
  2. 02指导功能优先的项目结构
  3. 03提供后端与前后端集成清单
  4. 04涵盖错误、数据库与认证模式
  5. 05要求构建、冒烟及集成验证

适用场景

  1. 01从零搭建全栈应用
  2. 02规划后端服务模块边界
  3. 03检查前后端 API 集成
  4. 04为生产环境加固服务

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/minimax-ai/skills --skill fullstack-dev

安装后在 Claude Code 中输入/fullstack-dev确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
api-design.md
auth-flow.md
db-schema.md
django-best-practices.md
environment-management.md
release-checklist.md
technology-selection.md
testing-strategy.md

详细介绍

全栈开发实践

强制工作流 — 按顺序完成以下步骤

触发此技能时,必须先遵循此工作流,再编写任何代码。

步骤 0:收集需求

在搭建任何内容之前,请向用户确认(或根据上下文推断):

  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)?

如果用户已在请求中说明这些信息,请跳过提问并继续。

步骤 1:架构决策

根据需求,在编写代码之前,做出并说明以下决策:

决策选项参考
项目结构功能优先(推荐)或分层优先第 1 节
API 客户端方案类型化 fetch / React Query / tRPC / OpenAPI 代码生成第 5 节
身份验证策略JWT + 刷新 / 会话 / 第三方第 6 节
实时通信方式轮询 / SSE / WebSocket第 11 节
错误处理类型化错误层级结构 + 全局处理器第 3 节

简要说明每项选择(每项决策用 1 句话)。

步骤 2:按照清单搭建

使用下方适用的清单。确保已实现所有勾选项 — 不要跳过任何一项。

步骤 3:按照模式实现

遵循本文档中的模式编写代码。实现每个部分时,请参考相应章节。

步骤 4:测试与验证

实现完成后,在宣告完成之前运行以下检查:

  1. 构建检查:确保后端和前端均可无错误编译
    # Backend
    cd server && npm run build
    # Frontend
    cd client && npm run build
    
  2. 启动与冒烟测试:启动服务,确认关键端点返回预期响应
    # Start server, then test
    curl http://localhost:3000/health
    curl http://localhost:3000/api/<resource>
    
  3. 集成检查:确认前端可连接后端(CORS、API 基础 URL、身份验证流程)
  4. 实时功能检查(如适用):打开两个浏览器标签页,确认更改能够同步

如果任何检查失败,请先修复问题再继续。

步骤 5:交付摘要

向用户提供简要摘要:

  • 已构建内容:列出已实现的功能和端点
  • 运行方式:启动后端和前端的确切命令
  • 缺失内容/后续步骤:任何延期事项、已知限制或建议的改进
  • 关键文件:列出用户需要了解的最重要文件

适用范围

在以下情况下使用此技能:

  • 构建全栈应用(后端 + 前端)
  • 搭建新的后端服务或 API
  • 设计服务层和模块边界
  • 实现数据库访问、缓存或后台任务
  • 编写错误处理、日志记录或配置管理
  • 审查后端代码中的架构问题
  • 为生产环境强化系统
  • 设置 API 客户端、身份验证流程、文件上传或实时功能

不适用于:

  • 纯前端/UI 问题(请参阅前端框架文档)
  • 不涉及后端的纯数据库架构设计

快速入门 — 新建后端服务检查清单

  • 项目已搭建,采用功能优先结构
  • 配置已集中管理,环境变量已在启动时验证(快速失败)
  • 已定义类型化错误层级(而非通用 Error)
  • 已配置全局错误处理器中间件
  • 使用结构化 JSON 日志,并传递请求 ID
  • 数据库:已设置迁移,已配置连接池
  • 所有端点均已进行输入验证(Zod / Pydantic / Go validator)
  • 已配置身份验证中间件
  • 已配置健康检查端点(/health、/ready)
  • 已处理优雅关闭(SIGTERM)
  • 已配置 CORS(明确指定来源,而非 *)
  • 已配置安全响应头(helmet 或同类工具)
  • 已提交 .env.example(不含真实密钥)

快速入门 — 前后端集成检查清单

  • 已配置 API 客户端(类型化 fetch 封装、React Query、tRPC 或由 OpenAPI 生成)
  • 基础 URL 来自环境变量(未硬编码)
  • 请求会自动附加身份验证令牌(拦截器 / 中间件)
  • 错误处理 — 将 API 错误映射为面向用户的消息
  • 已处理加载状态(骨架屏 / 加载动画,而非空白屏幕)
  • 跨边界保持类型安全(共享类型、OpenAPI 或 tRPC)
  • 已配置 CORS,并明确指定来源(生产环境中不使用 *)
  • 已实现刷新令牌流程(httpOnly Cookie + 收到 401 时透明重试)

快速导航

需要……跳转至
组织项目文件夹1. 项目结构
管理配置和密钥2. 配置
正确处理错误3. 错误处理
编写数据库代码4. 数据库访问模式
从前端设置 API 客户端5. API 客户端模式
添加身份验证中间件6. 身份验证与中间件
设置日志记录7. 日志记录与可观测性
添加后台任务8. 后台任务
实现缓存9. 缓存
上传文件(预签名 URL、分段上传)10. 文件上传模式
添加实时功能(SSE、WebSocket)11. 实时模式
在前端 UI 中处理 API 错误12. 跨边界错误处理
加固生产环境13. 生产环境加固
设计 API 端点API 设计
设计数据库架构数据库架构
身份验证流程(JWT、刷新、Next.js SSR、RBAC)references/auth-flow.md
CORS、环境变量、环境管理references/environment-management.md

核心原则(7 条铁律)

1. ✅ Organize by FEATURE, not by technical layer
2. ✅ Controllers never contain business logic
3. ✅ Services never import HTTP request/response types
4. ✅ All config from env vars, validated at startup, fail fast
5. ✅ Every error is typed, logged, and returns consistent format
6. ✅ All input validated at the boundary — trust nothing from client
7. ✅ Structured JSON logging with request ID — not console.log

1. 项目结构与分层(关键)

功能优先的组织方式
✅ Feature-first                    ❌ Layer-first
src/                                src/
  orders/                             controllers/
    order.controller.ts                 order.controller.ts
    order.service.ts                    user.controller.ts
    order.repository.ts               services/
    order.dto.ts                        order.service.ts
    order.test.ts                       user.service.ts
  users/                              repositories/
    user.controller.ts                  ...
    user.service.ts
  shared/
    database/
    middleware/
三层架构
Controller (HTTP) → Service (Business Logic) → Repository (Data Access)
层职责❌ 绝不包含
控制器解析请求、验证、调用服务、格式化响应业务逻辑、数据库查询
服务业务规则、流程编排、事务管理HTTP 类型(req/res)、直接访问数据库
仓储数据库查询、外部 API 调用业务逻辑、HTTP 类型
依赖注入(所有语言)

TypeScript:

class OrderService {
  constructor(
    private readonly orderRepo: OrderRepository,    // ✅ injected interface
    private readonly emailService: EmailService,
  ) {}
}

Python:

class OrderService:
    def __init__(self, order_repo: OrderRepository, email_service: EmailService):
        self.order_repo = order_repo                 # ✅ injected
        self.email_service = email_service

Go:

type OrderService struct {
    orderRepo    OrderRepository                      // ✅ interface
    emailService EmailService
}

func NewOrderService(repo OrderRepository, email EmailService) *OrderService {
    return &OrderService{orderRepo: repo, emailService: email}
}

2. 配置与环境(关键)

集中管理、类型化、快速失败

TypeScript:

const config = {
  port: parseInt(process.env.PORT || '3000', 10),
  database: { url: requiredEnv('DATABASE_URL'), poolSize: intEnv('DB_POOL_SIZE', 10) },
  auth: { jwtSecret: requiredEnv('JWT_SECRET'), expiresIn: process.env.JWT_EXPIRES_IN || '1h' },
} as const;

function requiredEnv(name: string): string {
  const value = process.env[name];
  if (!value) throw new Error(`Missing required env var: ${name}`);  // fail fast
  return value;
}

Python:

from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    database_url: str                        # required — app won't start without it
    jwt_secret: str                          # required
    port: int = 3000                         # optional with default
    db_pool_size: int = 10
    class Config:
        env_file = ".env"

settings = Settings()                        # fails fast if DATABASE_URL missing
规则
✅ All config via environment variables (Twelve-Factor)
✅ Validate required vars at startup — fail fast
✅ Type-cast at config layer, not at usage sites
✅ Commit .env.example with dummy values

❌ Never hardcode secrets, URLs, or credentials
❌ Never commit .env files
❌ Never scatter process.env / os.environ throughout code

3. 错误处理与弹性(高优先级)

类型化错误层级
// Base (TypeScript)
class AppError extends Error {
  constructor(
    message: string,
    public readonly code: string,
    public readonly statusCode: number,
    public readonly isOperational: boolean = true,
  ) { super(message); }
}
class NotFoundError extends AppError {
  constructor(resource: string, id: string) {
    super(`${resource} not found: ${id}`, 'NOT_FOUND', 404);
  }
}
class ValidationError extends AppError {
  constructor(public readonly errors: FieldError[]) {
    super('Validation failed', 'VALIDATION_ERROR', 422);
  }
}
# Base (Python)
class AppError(Exception):
    def __init__(self, message: str, code: str, status_code: int):
        self.message, self.code, self.status_code = message, code, status_code

class NotFoundError(AppError):
    def __init__(self, resource: str, id: str):
        super().__init__(f"{resource} not found: {id}", "NOT_FOUND", 404)
全局错误处理
// TypeScript (Express)
app.use((err, req, res, next) => {
  if (err instanceof AppError && err.isOperational) {
    return res.status(err.statusCode).json({
      title: err.code, status: err.statusCode,
      detail: err.message, request_id: req.id,
    });
  }
  logger.error('Unexpected error', { error: err.message, stack: err.stack, request_id: req.id });
  res.status(500).json({ title: 'Internal Error', status: 500, request_id: req.id });
});
规则
✅ Typed, domain-specific error classes
✅ Global error handler catches everything
✅ Operational errors → structured response
✅ Programming errors → log + generic 500
✅ Retry transient failures with exponential backoff

❌ Never catch and ignore errors silently
❌ Never return stack traces to client
❌ Never throw generic Error('something')

4. 数据库访问模式(高)

始终使用迁移
# TypeScript (Prisma)           # Python (Alembic)              # Go (golang-migrate)
npx prisma migrate dev          alembic revision --autogenerate  migrate -source file://migrations
npx prisma migrate deploy       alembic upgrade head             migrate -database $DB up
✅ Schema changes via migrations, never manual SQL
✅ Migrations must be reversible
✅ Review migration SQL before production
❌ Never modify production schema manually
防止 N+1 查询
// ❌ N+1: 1 query + N queries
const orders = await db.order.findMany();
for (const o of orders) { o.items = await db.item.findMany({ where: { orderId: o.id } }); }

// ✅ Single JOIN query
const orders = await db.order.findMany({ include: { items: true } });
多步骤写入使用事务
await db.$transaction(async (tx) => {
  const order = await tx.order.create({ data: orderData });
  await tx.inventory.decrement({ productId, quantity });
  await tx.payment.create({ orderId: order.id, amount });
});
连接池

池大小 = (CPU 核心数 × 2) + 磁盘数(从 10-20 开始)。始终设置连接超时。无服务器场景使用 PgBouncer。


5. API 客户端模式(中)

这是前端与后端之间的“胶合层”。选择适合团队和技术栈的方案。

方案 A:类型化 Fetch 封装(简单,无依赖)
// lib/api-client.ts
const BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';

class ApiError extends Error {
  constructor(public status: number, public body: any) {
    super(body?.detail || body?.message || `API error ${status}`);
  }
}

async function api<T>(path: string, options: RequestInit = {}): Promise<T> {
  const token = getAuthToken();  // from cookie / memory / context

  const res = await fetch(`${BASE_URL}${path}`, {
    ...options,
    headers: {
      'Content-Type': 'application/json',
      ...(token ? { Authorization: `Bearer ${token}` } : {}),
      ...options.headers,
    },
  });

  if (!res.ok) {
    const body = await res.json().catch(() => null);
    throw new ApiError(res.status, body);
  }

  if (res.status === 204) return undefined as T;
  return res.json();
}

export const apiClient = {
  get: <T>(path: string) => api<T>(path),
  post: <T>(path: string, data: unknown) => api<T>(path, { method: 'POST', body: JSON.stringify(data) }),
  put: <T>(path: string, data: unknown) => api<T>(path, { method: 'PUT', body: JSON.stringify(data) }),
  patch: <T>(path: string, data: unknown) => api<T>(path, { method: 'PATCH', body: JSON.stringify(data) }),
  delete: <T>(path: string) => api<T>(path, { method: 'DELETE' }),
};
方案 B:React Query + 类型化客户端(推荐用于 React)
// hooks/use-orders.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { apiClient } from '@/lib/api-client';

interface Order { id: string; total: number; status: string; }
interface CreateOrderInput { items: { productId: string; quantity: number }[] }

export function useOrders() {
  return useQuery({
    queryKey: ['orders'],
    queryFn: () => apiClient.get<{ data: Order[] }>('/api/orders'),
    staleTime: 1000 * 60,  // 1 min
  });
}

export function useCreateOrder() {
  const queryClient = useQueryClient();
  return useMutation({
    mutationFn: (data: CreateOrderInput) =>
      apiClient.post<{ data: Order }>('/api/orders', data),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['orders'] });
    },
  });
}

// Usage in component:
function OrdersPage() {
  const { data, isLoading, error } = useOrders();
  const createOrder = useCreateOrder();
  if (isLoading) return <Skeleton />;
  if (error) return <ErrorBanner error={error} />;
  // ...
}
方案 C:tRPC(同一团队负责两端)
// server: trpc/router.ts
export const appRouter = router({
  orders: router({
    list: publicProcedure.query(async () => {
      return db.order.findMany({ include: { items: true } });
    }),
    create: protectedProcedure
      .input(z.object({ items: z.array(orderItemSchema) }))
      .mutation(async ({ input, ctx }) => {
        return orderService.create(ctx.user.id, input);
      }),
  }),
});
export type AppRouter = typeof appRouter;

// client: automatic type safety, no code generation
const { data } = trpc.orders.list.useQuery();
const createOrder = trpc.orders.create.useMutation();
方案 D:OpenAPI 生成的客户端(公共 API / 多消费者 API)
npx openapi-typescript-codegen \
  --input http://localhost:3001/api/openapi.json \
  --output src/generated/api \
  --client axios
决策:选择哪种 API 客户端?
方案适用场景类型安全工作量
类型化 Fetch 封装简单应用、小型团队手动类型低
React Query + FetchReact 应用、服务端状态手动类型中
tRPC同一团队、两端均使用 TypeScript自动低
OpenAPI 生成公共 API、多消费者自动中
GraphQL codegenGraphQL API自动中

6. 身份验证与中间件(高)

完整参考: references/auth-flow.md — JWT bearer 流程、自动刷新令牌、Next.js 服务端身份验证、RBAC 模式、后端中间件顺序。

标准中间件顺序
Request → 1.RequestID → 2.Logging → 3.CORS → 4.RateLimit → 5.BodyParse
       → 6.Auth → 7.Authz → 8.Validation → 9.Handler → 10.ErrorHandler → Response
JWT 规则
✅ Short expiry access token (15min) + refresh token (server-stored)
✅ Minimal claims: userId, roles (not entire user object)
✅ Rotate signing keys periodically

❌ Never store tokens in localStorage (XSS risk)
❌ Never pass tokens in URL query params
RBAC 模式
function authorize(...roles: Role[]) {
  return (req, res, next) => {
    if (!req.user) throw new UnauthorizedError();
    if (!roles.some(r => req.user.roles.includes(r))) throw new ForbiddenError();
    next();
  };
}
router.delete('/users/:id', authenticate, authorize('admin'), deleteUser);
身份验证令牌自动刷新
// lib/api-client.ts — transparent refresh on 401
async function apiWithRefresh<T>(path: string, options: RequestInit = {}): Promise<T> {
  try {
    return await api<T>(path, options);
  } catch (err) {
    if (err instanceof ApiError && err.status === 401) {
      const refreshed = await api<{ accessToken: string }>('/api/auth/refresh', {
        method: 'POST',
        credentials: 'include',  // send httpOnly cookie
      });
      setAuthToken(refreshed.accessToken);
      return api<T>(path, options);  // retry
    }
    throw err;
  }
}

7. 日志记录与可观测性(中高)

结构化 JSON 日志
// ✅ Structured — parseable, filterable, alertable
logger.info('Order created', {
  orderId: order.id, userId: user.id, total: order.total,
  items: order.items.length, duration_ms: Date.now() - startTime,
});
// Output: {"level":"info","msg":"Order created","orderId":"ord_123",...}

// ❌ Unstructured — useless at scale
console.log(`Order created for user ${user.id} with total ${order.total}`);
日志级别
级别适用场景生产环境?
error需要立即关注✅ 始终
warn意料之外但已处理✅ 始终
info正常操作、审计跟踪✅ 始终
debug开发调试❌ 仅开发环境
规则
✅ Request ID in every log entry (propagated via middleware)
✅ Log at layer boundaries (request in, response out, external call)
❌ Never log passwords, tokens, PII, or secrets
❌ Never use console.log in production code

8. 后台任务与异步处理(中)

规则
✅ All jobs must be IDEMPOTENT (same job running twice = same result)
✅ Failed jobs → retry (max 3) → dead letter queue → alert
✅ Workers run as SEPARATE processes (not threads in API server)

❌ Never put long-running tasks in request handlers
❌ Never assume job runs exactly once
幂等任务模式
async function processPayment(data: { orderId: string }) {
  const order = await orderRepo.findById(data.orderId);
  if (order.paymentStatus === 'completed') return;  // already processed
  await paymentGateway.charge(order);
  await orderRepo.updatePaymentStatus(order.id, 'completed');
}

9. 缓存模式(中)

Cache-Aside(延迟加载)
async function getUser(id: string): Promise<User> {
  const cached = await redis.get(`user:${id}`);
  if (cached) return JSON.parse(cached);

  const user = await userRepo.findById(id);
  if (!user) throw new NotFoundError('User', id);

  await redis.set(`user:${id}`, JSON.stringify(user), 'EX', 900);  // 15min TTL
  return user;
}
规则
✅ ALWAYS set TTL — never cache without expiry
✅ Invalidate on write (delete cache key after update)
✅ Use cache for reads, never for authoritative state

❌ Never cache without TTL (stale data is worse than slow data)
数据类型建议 TTL
用户资料5-15 分钟
产品目录1-5 分钟
配置 / 功能标志30-60 秒
会话与会话时长一致

10. 文件上传模式(中)

方案 A:预签名 URL(大文件推荐)
Client → GET /api/uploads/presign?filename=photo.jpg&type=image/jpeg
Server → { uploadUrl: "https://s3.../presigned", fileKey: "uploads/abc123.jpg" }
Client → PUT uploadUrl (direct to S3, bypasses your server)
Client → POST /api/photos { fileKey: "uploads/abc123.jpg" }  (save reference)

后端:

app.get('/api/uploads/presign', authenticate, async (req, res) => {
  const { filename, type } = req.query;
  const key = `uploads/${crypto.randomUUID()}-${filename}`;
  const url = await s3.getSignedUrl('putObject', {
    Bucket: process.env.S3_BUCKET, Key: key,
    ContentType: type, Expires: 300,  // 5 min
  });
  res.json({ uploadUrl: url, fileKey: key });
});

前端:

async function uploadFile(file: File) {
  const { uploadUrl, fileKey } = await apiClient.get<PresignResponse>(
    `/api/uploads/presign?filename=${file.name}&type=${file.type}`
  );
  await fetch(uploadUrl, { method: 'PUT', body: file, headers: { 'Content-Type': file.type } });
  return apiClient.post('/api/photos', { fileKey });
}
方案 B:Multipart(小于 10MB 的文件)
// Frontend
const formData = new FormData();
formData.append('file', file);
formData.append('description', 'Profile photo');
const res = await fetch('/api/upload', { method: 'POST', body: formData });
// Note: do NOT set Content-Type header — browser sets boundary automatically
决策
方法文件大小服务器负载复杂度
预签名 URL任意(推荐大于 5MB)无(直接传到存储)中
Multipart小于 10MB高(经由服务器传输)低
分块 / 可恢复上传大于 100MB中高

11. 实时模式(中)

方案 A:Server-Sent Events(SSE)— 单向:服务器 → 客户端

适用于:通知、实时动态、流式 AI 响应。

后端(Express):

app.get('/api/events', authenticate, (req, res) => {
  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    Connection: 'keep-alive',
  });
  const send = (event: string, data: unknown) => {
    res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
  };
  const unsubscribe = eventBus.subscribe(req.user.id, (event) => {
    send(event.type, event.payload);
  });
  req.on('close', () => unsubscribe());
});

前端:

function useServerEvents(userId: string) {
  useEffect(() => {
    const source = new EventSource(`/api/events?userId=${userId}`);
    source.addEventListener('notification', (e) => {
      showToast(JSON.parse(e.data).message);
    });
    source.onerror = () => { source.close(); setTimeout(() => /* reconnect */, 3000); };
    return () => source.close();
  }, [userId]);
}
方案 B:WebSocket — 双向

适用于:聊天、协同编辑、游戏。

后端(ws 库):

import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ server: httpServer, path: '/ws' });
wss.on('connection', (ws, req) => {
  const userId = authenticateWs(req);
  if (!userId) { ws.close(4001, 'Unauthorized'); return; }
  ws.on('message', (raw) => handleMessage(userId, JSON.parse(raw.toString())));
  ws.on('close', () => cleanupUser(userId));
  const interval = setInterval(() => ws.ping(), 30000);
  ws.on('pong', () => { /* alive */ });
  ws.on('close', () => clearInterval(interval));
});

前端:

function useWebSocket(url: string) {
  const [ws, setWs] = useState<WebSocket | null>(null);
  useEffect(() => {
    const socket = new WebSocket(url);
    socket.onopen = () => setWs(socket);
    socket.onclose = () => setTimeout(() => /* reconnect */, 3000);
    return () => socket.close();
  }, [url]);
  const send = useCallback((data: unknown) => ws?.send(JSON.stringify(data)), [ws]);
  return { ws, send };
}
方案 C:轮询(最简单,无需基础设施)
function useOrderStatus(orderId: string) {
  return useQuery({
    queryKey: ['order-status', orderId],
    queryFn: () => apiClient.get<Order>(`/api/orders/${orderId}`),
    refetchInterval: (query) => {
      if (query.state.data?.status === 'completed') return false;
      return 5000;
    },
  });
}
决策
方法方向复杂度适用场景
轮询客户端 → 服务器低简单状态检查,少于 10 个客户端
SSE服务器 → 客户端中通知、动态、AI 流式响应
WebSocket双向高聊天、协作、游戏

12. 跨边界错误处理(中)

API 错误 → 面向用户的消息
// lib/error-handler.ts
export function getErrorMessage(error: unknown): string {
  if (error instanceof ApiError) {
    switch (error.status) {
      case 401: return 'Please log in to continue.';
      case 403: return 'You don\'t have permission to do this.';
      case 404: return 'The item you\'re looking for doesn\'t exist.';
      case 409: return 'This conflicts with an existing item.';
      case 422:
        const fields = error.body?.errors;
        if (fields?.length) return fields.map((f: any) => f.message).join('. ');
        return 'Please check your input.';
      case 429: return 'Too many requests. Please wait a moment.';
      default: return 'Something went wrong. Please try again.';
    }
  }
  if (error instanceof TypeError && error.message === 'Failed to fetch') {
    return 'Cannot connect to server. Check your internet connection.';
  }
  return 'An unexpected error occurred.';
}
React Query 全局错误处理器
const queryClient = new QueryClient({
  defaultOptions: {
    mutations: { onError: (error) => toast.error(getErrorMessage(error)) },
    queries: {
      retry: (failureCount, error) => {
        if (error instanceof ApiError && error.status < 500) return false;
        return failureCount < 3;
      },
    },
  },
});
规则
✅ Map every API error code to a human-readable message
✅ Show field-level validation errors next to form inputs
✅ Auto-retry on 5xx (max 3, with backoff), never on 4xx
✅ Redirect to login on 401 (after refresh attempt fails)
✅ Show "offline" banner when fetch fails with TypeError

❌ Never show raw API error messages to users ("NullPointerException")
❌ Never silently swallow errors (show toast or log)
❌ Never retry 4xx errors (client is wrong, retrying won't help)
集成决策树
Same team owns frontend + backend?
│
├─ YES, both TypeScript
│   └─ tRPC (end-to-end type safety, zero codegen)
│
├─ YES, different languages
│   └─ OpenAPI spec → generated client (type safety via codegen)
│
├─ NO, public API
│   └─ REST + OpenAPI → generated SDKs for consumers
│
└─ Complex data needs, multiple frontends
    └─ GraphQL + codegen (flexible queries per client)

Real-time needed?
│
├─ Server → Client only (notifications, feeds, AI streaming)
│   └─ SSE (simplest, auto-reconnect, works through proxies)
│
├─ Bidirectional (chat, collaboration)
│   └─ WebSocket (need heartbeat + reconnection logic)
│
└─ Simple status polling (< 10 clients)
    └─ React Query refetchInterval (no infrastructure needed)

13. 生产环境加固(中)

健康检查
app.get('/health', (req, res) => res.json({ status: 'ok' }));           // liveness
app.get('/ready', async (req, res) => {                                   // readiness
  const checks = {
    database: await checkDb(), redis: await checkRedis(), 
  };
  const ok = Object.values(checks).every(c => c.status === 'ok');
  res.status(ok ? 200 : 503).json({ status: ok ? 'ok' : 'degraded', checks });
});
优雅关闭
process.on('SIGTERM', async () => {
  logger.info('SIGTERM received');
  server.close();              // stop new connections
  await drainConnections();    // finish in-flight
  await closeDatabase();
  process.exit(0);
});
安全检查清单
✅ CORS: explicit origins (never '*' in production)
✅ Security headers (helmet / equivalent)
✅ Rate limiting on public endpoints
✅ Input validation on ALL endpoints (trust nothing)
✅ HTTPS enforced
❌ Never expose internal errors to clients

反模式

#❌ 不要✅ 改为
1在路由/控制器中编写业务逻辑移至服务层
2到处散落 process.env集中管理类型化配置
3使用 console.log 记录日志使用结构化 JSON 日志记录器
4使用通用的 Error('oops')使用类型化错误层级
5在控制器中直接调用数据库使用仓储模式
6不验证输入在边界处验证(Zod/Pydantic)
7静默捕获错误记录日志并重新抛出,或返回错误
8没有健康检查端点添加 /health + /ready
9硬编码配置/密钥使用环境变量
10未实现优雅关闭正确处理 SIGTERM
11在前端硬编码 API URL使用环境变量(NEXT_PUBLIC_API_URL)
12将 JWT 存储在 localStorage 中存储在内存中,并使用 httpOnly 刷新 Cookie
13向用户显示原始 API 错误映射为易于理解的消息
14重试 4xx 错误仅重试 5xx(服务器故障)
15跳过加载状态获取数据时显示骨架屏/加载指示器
16通过 API 服务器上传大文件使用预签名 URL → 直接上传到 S3
17轮询实时数据使用 SSE 或 WebSocket
18在前端和后端重复定义类型使用共享类型、tRPC 或 OpenAPI 代码生成

常见问题

问题 1:“业务规则应该放在哪里?”

**规则:**如果涉及 HTTP(请求解析、状态码、标头)→ 放在控制器中。如果涉及业务决策(定价、权限、规则)→ 放在服务中。如果涉及数据库 → 放在仓储中。

问题 2:“服务越来越庞大”

**现象:**单个服务文件超过 500 行,且包含 20 多个方法。

**解决方法:**按子领域拆分。OrderService → OrderCreationService + OrderFulfillmentService + OrderQueryService。每个服务专注于一个工作流。

问题 3:“测试会访问数据库,因此运行很慢”

**解决方法:**单元测试模拟仓储层(速度快)。集成测试使用测试容器或事务回滚(使用真实数据库,但仍然很快)。集成测试中切勿模拟服务层。


参考文档

此技能包含针对专业主题的深入参考资料。需要详细指导时,请阅读相关参考文档。

需要…参考文档
编写后端测试(单元、集成、端到端、契约、性能)references/testing-strategy.md
部署前验证发布内容(6 道关卡检查清单)references/release-checklist.md
选择技术栈(语言、框架、数据库、基础设施)references/technology-selection.md
使用 Django / DRF 构建(模型、视图、序列化器、管理后台)references/django-best-practices.md
设计 REST/GraphQL/gRPC 端点(URL、状态码、分页)references/api-design.md
设计数据库架构、索引、迁移、多租户references/db-schema.md
认证流程(JWT bearer、令牌刷新、Next.js SSR、RBAC、中间件顺序)references/auth-flow.md
配置 CORS、设置各环境的环境变量、排查常见 CORS 问题references/environment-management.md

评论

支持 Markdown