安装方式
命令行安装
在项目根目录执行以下命令,完成 Skill 安装。
npx bzskills add MiniMax-AI/skills --skill fullstack-dev 全栈后端架构与前后端集成指南。 触发条件:构建全栈应用、创建带有前端的REST API、搭建后端服务、 构建待办事项应用、构建CRUD应用、构建实时应用、构建聊天应用、 使用Express + React、Next.js API、Node.js后端、Python后端、Go后端、 设计服务层、实现错误处理、管理配置/认证、 设置API客户端、实现认证流程、处理文件上传、 添加实时功能(SSE/WebSocket)、为生产环境加固。 不触发条件:纯前端UI工作、纯CSS/样式、仅数据库模式。
38
下载量
命令行安装
在项目根目录执行以下命令,完成 Skill 安装。
npx bzskills add MiniMax-AI/skills --skill fullstack-dev name: fullstack-dev
description: |-
全栈后端架构与前后端集成指南。
触发条件:构建全栈应用、创建带有前端的REST API、搭建后端服务、
构建待办事项应用、构建CRUD应用、构建实时应用、构建聊天应用、
使用Express + React、Next.js API、Node.js后端、Python后端、Go后端、
设计服务层、实现错误处理、管理配置/认证、
设置API客户端、实现认证流程、处理文件上传、
添加实时功能(SSE/WebSocket)、为生产环境加固。
不触发条件:纯前端UI工作、纯CSS/样式、仅数据库模式。
license: MIT
metadata:
category: full-stack
version: "1.0.0"
sources:
- The Twelve-Factor App (12factor.net)
- Clean Architecture (Robert C. Martin)
- Domain-Driven Design (Eric Evans)
- Patterns of Enterprise Application Architecture (Martin Fowler)
- Martin Fowler (Testing Pyramid, Contract Tests)
- Google SRE Handbook (Release Engineering)
- ThoughtWorks Technology Radar当此技能被触发时,在编写任何代码之前,你必须遵循此工作流程。
在搭建任何内容之前,请让用户澄清(或从上下文中推断):
如果用户已在请求中指定了这些内容,则跳过提问并继续。
根据需求,在编码前做出并说明以下决策:
| 决策 | 选项 | 参考 |
|---|---|---|
| 项目结构 | 按功能优先(推荐) vs 按层次优先 | 第 1 节 |
| API 客户端方法 | 类型化 fetch / React Query / tRPC / OpenAPI 代码生成 | 第 5 节 |
| 认证策略 | JWT + 刷新 / 会话 / 第三方 | 第 6 节 |
| 实时方法 | 轮询 / SSE / WebSocket | 第 11 节 |
| 错误处理 | 类型化错误层次结构 + 全局处理器 | 第 3 节 |
简要解释每个选择(每个决策一句话)。
使用以下适当的清单。确保实现所有勾选的项 — 不要跳过任何项。
按照本文档中的模式编写代码。在实现每个部分时引用特定章节。
实现后,在声明完成前运行以下检查:
# 后端
cd server && npm run build
# 前端
cd client && npm run build
# 启动服务器,然后测试
curl http://localhost:3000/health
curl http://localhost:3000/api/<resource>
如果任何检查失败,在继续前修复问题。
向用户提供简要总结:
---
使用此技能的场景:
不适用于:
---
Error)/health、/ready)*).env.example(无真实密钥)*)---
| 需要… | 跳转到 |
|---|---|
| 组织项目文件夹 | 1. 项目结构 |
| 管理配置+密钥 | 2. 配置 |
| 正确处理错误 | 3. 错误处理 |
| 编写数据库代码 | 4. 数据库访问模式 |
| 从前端设置 API 客户端 | 5. API 客户端模式 |
| 添加认证中间件 | 6. 认证与中间件 |
| 设置日志记录 | 7. 日志记录与可观测性 |
| 添加后台任务 | 8. 后台任务 |
| 实现缓存 | 9. 缓存模式 |
| 文件上传(预签名 URL、multipart) | 10. 文件上传模式 |
| 添加实时功能(SSE、WebSocket) | 11. 实时模式 |
| 在前端 UI 中处理 API 错误 | 12. 跨边界错误处理 |
| 生产环境加固 | 13. 生产环境加固 |
| 设计 API 端点 | [API 设计](references/api-design.md) |
| 设计数据库模式 | [数据库模式](references/db-schema.md) |
| 认证流程(JWT、刷新、Next.js SSR、RBAC) | [references/auth-flow.md](references/auth-flow.md) |
| CORS、环境变量、环境管理 | [references/environment-management.md](references/environment-management.md) |
---
1. ✅ 按**功能**组织,而非按技术层次
2. ✅ 控制器绝不包含业务逻辑
3. ✅ 服务绝不导入 HTTP 请求/响应类型
4. ✅ 所有配置来自环境变量,启动时验证,快速失败
5. ✅ 每个错误都是类型化的,记录日志,并返回一致格式
6. ✅ 所有输入在边界处验证 — 不信任客户端的任何内容
7. ✅ 结构化 JSON 日志记录,包含请求 ID — 而不是 console.log
---
✅ 功能优先 ❌ 层次优先
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/
控制器 (HTTP) → 服务 (业务逻辑) → 仓库 (数据访问)
| 层 | 职责 | ❌ 绝不 |
|---|---|---|
| 控制器 | 解析请求、验证、调用服务、格式化响应 | 业务逻辑、数据库查询 |
| 服务 | 业务规则、编排、事务管理 | HTTP 类型(请求/响应)、直接数据库 |
| 仓库 | 数据库查询、外部 API 调用 | 业务逻辑、HTTP 类型 |
TypeScript:
class OrderService {
constructor(
private readonly orderRepo: OrderRepository, // ✅ 注入接口
private readonly emailService: EmailService,
) {}
}
Python:
class OrderService:
def __init__(self, order_repo: OrderRepository, email_service: EmailService):
self.order_repo = order_repo # ✅ 注入
self.email_service = email_service
Go:
type OrderService struct {
orderRepo OrderRepository // ✅ 接口
emailService EmailService
}
func NewOrderService(repo OrderRepository, email EmailService) *OrderService {
return &OrderService{orderRepo: repo, emailService: email}
}
---
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}`); // 快速失败
return value;
}
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 缺失,快速失败
✅ 所有配置通过环境变量(十二因子)
✅ 在启动时验证必需变量 — 快速失败
✅ 在配置层进行类型转换,而非在使用处
✅ 提交包含虚拟值的 .env.example
❌ 绝不硬编码密钥、URL 或凭据
❌ 绝不提交 .env 文件
❌ 绝不将 process.env / os.environ 分散在代码中
---
// 基础 (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)
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 });
});
✅ 类型化、领域特定的错误类
✅ 全局错误处理器捕获所有异常
✅ 操作性错误 → 结构化响应
✅ 编程错误 → 记录日志 + 通用 500
✅ 使用指数退避重试临时性故障
❌ 绝不捕获错误后静默忽略
❌ 绝不向客户端返回堆栈跟踪
❌ 绝不抛出通用 Error('something')
---
# 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
✅ 模式变更通过迁移完成,绝不手动 SQL
✅ 迁移必须可逆
✅ 生产环境前审查迁移 SQL
❌ 绝不手动修改生产环境模式
// ❌ 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 } });
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) + spindle_count(从 10-20 开始)。始终设置连接超时。服务器无状态场景使用 PgBouncer。
---
前端和后端之间的“粘合层”。选择适合你的团队和技术栈的方法。
// 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(); // 来自 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' }),
};
// 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} />;
// ...
}
// 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();
npx openapi-typescript-codegen \
--input http://localhost:3001/api/openapi.json \
--output src/generated/api \
--client axios
| 方法 | 适用场景 | 类型安全 | 工作量 |
|---|---|---|---|
| 类型化 fetch 包装器 | 简单应用、小团队 | 手动类型 | 低 |
| React Query + fetch | React 应用、服务端状态 | 手动类型 | 中等 |
| tRPC | 同一团队、两侧 TypeScript | 自动 | 低 |
| OpenAPI 生成 | 公共 API、多消费者 | 自动 | 中等 |
| GraphQL codegen | GraphQL API | 自动 | 中等 |
---
完整参考: [references/auth-flow.md](references/auth-flow.md) — JWT bearer 流程、自动令牌刷新、Next.js 服务端认证、RBAC 模式、后端中间件顺序。
请求 → 1.RequestID → 2.Logging → 3.CORS → 4.RateLimit → 5.BodyParse
→ 6.Auth → 7.Authz → 8.Validation → 9.Handler → 10.ErrorHandler → 响应
✅ 短有效期的访问令牌(15分钟)+ 刷新令牌(服务端存储)
✅ 最小化声明:userId、roles(不包含整个用户对象)
✅ 定期轮换签名密钥
❌ 绝不将令牌存储在 localStorage(XSS 风险)
❌ 绝不在 URL 查询参数中传递令牌
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 — 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;
}
}
---
// ✅ 结构化 — 可解析、可过滤、可告警
logger.info('Order created', {
orderId: order.id, userId: user.id, total: order.total,
items: order.items.length, duration_ms: Date.now() - startTime,
});
// 输出: {"level":"info","msg":"Order created","orderId":"ord_123",...}
// ❌ 非结构化 — 在大规模下无用
console.log(`Order created for user ${user.id} with total ${order.total}`);
| 级别 | 何时使用 | 生产环境 |
|---|---|---|
| error | 需要立即关注 | ✅ 始终 |
| warn | 意外但已处理 | ✅ 始终 |
| info | 正常操作、审计追踪 | ✅ 始终 |
| debug | 开发调试 | ❌ 仅开发环境 |
✅ 每条日志条目包含请求 ID(通过中间件传播)
✅ 在层边界记录日志(请求进入、响应离开、外部调用)
❌ 绝不记录密码、令牌、PII 或密钥
❌ 绝不在生产代码中使用 console.log
---
✅ 所有任务必须是**幂等**的(同一任务运行两次 = 相同结果)
✅ 失败任务 → 重试(最多 3 次)→ 死信队列 → 告警
✅ 工作者作为**独立进程**运行(非 API 服务器中的线程)
❌ 绝不将长时间运行的任务放在请求处理程序中
❌ 绝不确定任务只运行一次
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');
}
---
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); // 15 分钟 TTL
return user;
}
✅ 始终设置 TTL — 绝不在没有过期时间的情况下缓存
✅ 写入时失效(更新后删除缓存键)
✅ 缓存用于读取,绝不用于权威状态
❌ 绝不缓存而不设置 TTL(过时数据比慢数据更糟糕)
| 数据类型 | 建议 TTL |
|---|---|
| 用户资料 | 5-15 分钟 |
| 产品目录 | 1-5 分钟 |
| 配置/功能标志 | 30-60 秒 |
| 会话 | 匹配会话持续时间 |
---
客户端 → 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" } (保存引用)
后端:
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 });
});
前端:
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 });
}
// 前端
const formData = new FormData();
formData.append('file', file);
formData.append('description', 'Profile photo');
const res = await fetch('/api/upload', { method: 'POST', body: formData });
// 注意:不要设置 Content-Type 头 — 浏览器会自动设置 boundary
| 方法 | 文件大小 | 服务器负载 | 复杂性 |
|---|---|---|---|
| 预签名 URL | 任意(推荐 > 5MB) | 无(直接到存储) | 中等 |
| Multipart | < 10MB | 高(流经服务器) | 低 |
| 分块/可续传 | > 100MB | 中等 | 高 |
---
最适合:通知、实时推送、流式 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(() => /* 重新连接 */, 3000); };
return () => source.close();
}, [userId]);
}
最适合:聊天、协同编辑、游戏。
后端(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', () => { /* 存活 */ });
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(() => /* 重新连接 */, 3000);
return () => socket.close();
}, [url]);
const send = useCallback((data: unknown) => ws?.send(JSON.stringify(data)), [ws]);
return { ws, send };
}
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 | 双向 | 高 | 聊天、协作、游戏 |
---
// 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 '发生了意外错误。';
}
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;
},
},
},
});
✅ 将每个 API 错误代码映射为人类可读的消息
✅ 在表单输入旁边显示字段级验证错误
✅ 5xx 自动重试(最多 3 次,带退避),4xx 永不重试
✅ 401 时重定向到登录(刷新尝试失败后)
✅ 当 fetch 失败并出现 TypeError 时显示“离线”横幅
❌ 绝不向用户显示原始 API 错误消息(“NullPointerException”)
❌ 绝不静默吞下错误(显示 toast 或记录日志)
❌ 绝不重试 4xx 错误(客户端错误,重试无济于事)
同一团队拥有前端+后端?
│
├─ 是,均为 TypeScript
│ └─ tRPC(端到端类型安全,零代码生成)
│
├─ 是,不同语言
│ └─ OpenAPI 规范 → 生成客户端(通过代码生成实现类型安全)
│
├─ 否,公共 API
│ └─ REST + OpenAPI → 为消费者生成 SDK
│
└─ 复杂数据需求,多个前端
└─ GraphQL + 代码生成(每个客户端灵活查询)
是否需要实时功能?
│
├─ 仅服务器 → 客户端(通知、推送、AI 流式输出)
│ └─ SSE(最简单,自动重连,可通过代理工作)
│
├─ 双向(聊天、协作)
│ └─ WebSocket(需要心跳 + 重连逻辑)
│
└─ 简单状态轮询(< 10 个客户端)
└─ React Query refetchInterval(无需基础设施)
---
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 });
});
process.on('SIGTERM', async () => {
logger.info('SIGTERM received');
server.close(); // 停止新连接
await drainConnections(); // 完成进行中的请求
await closeDatabase();
process.exit(0);
});
✅ CORS:显式来源(生产环境中绝不使用 '*')
✅ 安全头(helmet / 等效)
✅ 公共端点的速率限制
✅ 所有端点的输入验证(不信任任何输入)
✅ 强制 HTTPS
❌ 绝不向客户端暴露内部错误
---
| # | ❌ 不要 | ✅ 应该 |
|---|---|---|
| 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 代码生成 |
---
规则: 如果涉及 HTTP(请求解析、状态码、头)→ 控制器。如果涉及业务决策(定价、权限、规则)→ 服务。如果涉及数据库 → 仓库。
症状: 一个服务文件超过 500 行,有 20 多个方法。
修复: 按子领域拆分。OrderService → OrderCreationService + OrderFulfillmentService + OrderQueryService。每个专注于一个工作流。
修复: 单元测试模拟仓库层(快速)。集成测试使用测试容器或事务回滚(真实数据库,仍然快速)。集成测试中绝不模拟服务层。
---
此技能包含针对特定主题的深入参考文档。当你需要详细指导时,请阅读相关参考。
| 需要… | 参考 |
|---|---|
| 编写后端测试(单元、集成、端到端、契约、性能) | [references/testing-strategy.md](references/testing-strategy.md) |
| 部署前验证发布(6 门检查清单) | [references/release-checklist.md](references/release-checklist.md) |
| 选择技术栈(语言、框架、数据库、基础设施) | [references/technology-selection.md](references/technology-selection.md) |
| 使用 Django / DRF 构建(模型、视图、序列化器、管理后台) | [references/django-best-practices.md](references/django-best-practices.md) |
| 设计 REST/GraphQL/gRPC 端点(URL、状态码、分页) | [references/api-design.md](references/api-design.md) |
| 设计数据库模式、索引、迁移、多租户 | [references/db-schema.md](references/db-schema.md) |
| 认证流程(JWT bearer、令牌刷新、Next.js SSR、RBAC、中间件顺序) | [references/auth-flow.md](references/auth-flow.md) |
| CORS 配置、各环境的环境变量、常见 CORS 问题 | [references/environment-management.md](references/environment-management.md) |