某中型企业正在开发一个使用TypeScript、React前端和Node.js后端的全栈应用。随着项目复杂度增加,团队面临以下挑战:
- 类型定义不一致 - 前后端对相同数据结构使用不同的类型定义,导致类型错误和数据处理问题
- API契约不明确 - 前后端之间的数据交换格式经常变动,没有明确的契约保障
- 代码重复 - 某些验证逻辑和工具函数在前后端重复实现,导致维护困难
- 开发流程割裂 - 前端和后端开发者使用不同的工作流程,协作效率低下
团队设计了一系列规则来改善全栈项目的一致性和协作效率:
<rule>
name: shared_type_definitions
description: 确保前后端使用相同的类型定义
filters:
# 匹配TypeScript类型定义文件
- type: file_extension
pattern: "\\.ts$|\\.d\\.ts$"
# 匹配包含类型定义的内容
- type: content
pattern: "(?:interface|type|enum)\\s+\\w+"
actions:
- type: review
criteria:
# 检查是否使用共享类型
- pattern: "import\\s+(?:\\{\\s*)?\\w+(?:\\s*\\})?\\s+from\\s+['\"]@shared/types['\"]"
message: "✓ 使用共享类型定义"
not_found_message: "✗ 应使用@shared/types中的共享类型定义"
# 检查模型类型的命名约定
- pattern: "(?:interface|type)\\s+I[A-Z]\\w+(?:Model|DTO|Request|Response)"
message: "✓ 遵循类型命名约定"
not_found_message: "✗ 模型类型应使用I前缀并以Model、DTO、Request或Response结尾"
optional: true
- type: create_file
conditions:
- pattern: "package\\.json"
file_path: "package.json"
not_found: false
- pattern: "@shared/types"
not_found: true
path: "src/shared/types/index.ts"
content: |
/**
* 共享类型定义
*
* 该文件包含前后端共享的类型定义,确保数据结构的一致性。
*/
/**
* 用户模型接口
*/
export interface IUserModel {
id: string;
username: string;
email: string;
role: UserRole;
createdAt: Date;
updatedAt: Date;
}
/**
* 用户角色枚举
*/
export enum UserRole {
ADMIN = 'admin',
USER = 'user',
GUEST = 'guest'
}
/**
* 用户创建请求DTO
*/
export interface IUserCreateRequestDTO {
username: string;
email: string;
password: string;
role?: UserRole;
}
/**
* 用户创建响应DTO
*/
export interface IUserCreateResponseDTO {
id: string;
username: string;
email: string;
role: UserRole;
}
/**
* API错误响应接口
*/
export interface IApiErrorResponse {
success: false;
error: {
code: string;
message: string;
details?: unknown;
};
}
/**
* API成功响应接口
*/
export interface IApiSuccessResponse<T> {
success: true;
data: T;
meta?: {
total?: number;
page?: number;
limit?: number;
};
}
/**
* 分页请求参数
*/
export interface IPaginationParams {
page?: number;
limit?: number;
sort?: string;
order?: 'asc' | 'desc';
}
/**
* 分页响应元数据
*/
export interface IPaginationMeta {
total: number;
page: number;
limit: number;
totalPages: number;
}
- type: suggest
message: |
建议按以下步骤组织和使用共享类型:
1. **设置项目结构**,创建共享类型目录:
```
src/
├── shared/ # 共享代码
│ ├── types/ # 共享类型定义
│ │ ├── models/ # 数据模型类型
│ │ ├── dtos/ # 数据传输对象类型
│ │ ├── enums/ # 枚举类型
│ │ └── index.ts # 主导出文件
│ ├── utils/ # 共享工具函数
│ └── validators/ # 共享验证逻辑
├── client/ # 前端代码
└── server/ # 后端代码
```
2. **配置TypeScript路径别名**,便于导入:
```json
// tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@shared/*": ["src/shared/*"],
"@client/*": ["src/client/*"],
"@server/*": ["src/server/*"]
}
}
}
```
3. **使用共享类型示例**:
后端控制器:
```typescript
// src/server/controllers/user.controller.ts
import {
IUserModel,
IUserCreateRequestDTO,
IUserCreateResponseDTO,
IApiSuccessResponse
} from '@shared/types';
export const createUser = async (req: Request, res: Response): Promise<void> => {
const userData: IUserCreateRequestDTO = req.body;
// 处理请求...
const response: IApiSuccessResponse<IUserCreateResponseDTO> = {
success: true,
data: {
id: user.id,
username: user.username,
email: user.email,
role: user.role
}
};
res.status(201).json(response);
};
```
前端组件:
```typescript
// src/client/components/UserForm.tsx
import { IUserCreateRequestDTO, UserRole } from '@shared/types';
const UserForm: React.FC = () => {
const [formData, setFormData] = useState<IUserCreateRequestDTO>({
username: '',
email: '',
password: '',
role: UserRole.USER
});
// 组件实现...
};
```
metadata:
priority: high
version: 1.0.0
tags: ["typescript", "full-stack", "shared-types"]
</rule>
<rule>
name: api_contract_validation
description: 确保前后端API交互遵循明确的契约
filters:
# 匹配API相关文件
- type: file_path
pattern: "(?:api|services|controllers|routes)/.*\\.(ts|js)$"
# 匹配API请求或响应相关内容
- type: content
pattern: "(?:fetch|axios|http|request|response|api)"
actions:
- type: review
criteria:
# 检查是否使用ZOD或其他运行时验证
- pattern: "import\\s+(?:\\{\\s*)?z(?:\\s*\\})?\\s+from\\s+['\"]zod['\"]|const\\s+\\w+Schema\\s*=\\s*z\\."
message: "✓ 使用Zod进行API契约验证"
not_found_message: "✗ 建议使用Zod等运行时验证库验证API数据"
# 检查API响应类型
- pattern: "type\\s+(?:ApiResponse|IApiResponse)\\s*<\\s*T\\s*>"
message: "✓ 使用泛型API响应类型"
not_found_message: "✗ 应使用泛型API响应类型"
optional: true
- type: create_file
conditions:
- pattern: "zod"
not_found: true
path: "src/shared/validators/schemas.ts"
content: |
/**
* API契约验证模式
*
* 使用Zod库定义API请求和响应的验证模式,确保数据符合预期格式。
*/
import { z } from 'zod';
import { UserRole } from '../types';
/**
* 用户创建请求验证模式
*/
export const userCreateRequestSchema = z.object({
username: z.string().min(3).max(50),
email: z.string().email(),
password: z.string().min(8).max(100),
role: z.nativeEnum(UserRole).optional()
});
/**
* 用户创建响应验证模式
*/
export const userCreateResponseSchema = z.object({
success: z.literal(true),
data: z.object({
id: z.string().uuid(),
username: z.string(),
email: z.string().email(),
role: z.nativeEnum(UserRole)
})
});
/**
* API错误响应验证模式
*/
export const apiErrorResponseSchema = z.object({
success: z.literal(false),
error: z.object({
code: z.string(),
message: z.string(),
details: z.unknown().optional()
})
});
/**
* 创建泛型API成功响应验证模式
*/
export const createApiSuccessResponseSchema = <T extends z.ZodType>(dataSchema: T) => {
return z.object({
success: z.literal(true),
data: dataSchema,
meta: z.object({
total: z.number().optional(),
page: z.number().optional(),
limit: z.number().optional()
}).optional()
});
};
/**
* 分页请求参数验证模式
*/
export const paginationParamsSchema = z.object({
page: z.number().min(1).optional().default(1),
limit: z.number().min(1).max(100).optional().default(10),
sort: z.string().optional(),
order: z.enum(['asc', 'desc']).optional().default('asc')
});
- type: suggest
message: |
建议按以下步骤实现API契约验证:
1. **安装Zod库**:
```bash
npm install zod
```
2. **在后端使用Zod验证请求数据**:
```typescript
// src/server/middlewares/validate.middleware.ts
import { Request, Response, NextFunction } from 'express';
import { AnyZodObject } from 'zod';
export const validate = (schema: AnyZodObject) =>
async (req: Request, res: Response, next: NextFunction) => {
try {
await schema.parseAsync(req.body);
return next();
} catch (error) {
return res.status(400).json({
success: false,
error: {
code: 'VALIDATION_ERROR',
message: '输入验证失败',
details: error.errors
}
});
}
};
// src/server/routes/user.routes.ts
import { Router } from 'express';
import { userController } from '../controllers';
import { validate } from '../middlewares';
import { userCreateRequestSchema } from '@shared/validators/schemas';
const router = Router();
router.post(
'/api/v1/users',
validate(userCreateRequestSchema),
userController.createUser
);
export default router;
```
3. **在前端使用Zod验证API响应**:
```typescript
// src/client/services/api.ts
import axios from 'axios';
import {
userCreateRequestSchema,
userCreateResponseSchema,
apiErrorResponseSchema
} from '@shared/validators/schemas';
import type { IUserCreateRequestDTO } from '@shared/types';
export const createUser = async (userData: IUserCreateRequestDTO) => {
try {
// 验证请求数据
userCreateRequestSchema.parse(userData);
const response = await axios.post('/api/v1/users', userData);
// 验证响应数据
const validatedResponse = userCreateResponseSchema.parse(response.data);
return validatedResponse;
} catch (error) {
if (error.response?.data) {
// 验证错误响应
const errorResponse = apiErrorResponseSchema.safeParse(error.response.data);
if (errorResponse.success) {
return errorResponse.data;
}
}
throw error;
}
};
```
4. **使用API模拟进行契约测试**:
```typescript
// src/tests/api-contract.test.ts
import { userCreateRequestSchema, userCreateResponseSchema } from '@shared/validators/schemas';
describe('API契约测试', () => {
test('用户创建API契约', async () => {
// 模拟请求数据
const requestData = {
username: 'testuser',
email: 'test@example.com',
password: 'password123'
};
// 验证请求数据
expect(() => userCreateRequestSchema.parse(requestData)).not.toThrow();
// 模拟响应数据
const responseData = {
success: true,
data: {
id: '123e4567-e89b-12d3-a456-426614174000',
username: 'testuser',
email: 'test@example.com',
role: 'user'
}
};
// 验证响应数据
expect(() => userCreateResponseSchema.parse(responseData)).not.toThrow();
});
});
```
metadata:
priority: high
version: 1.0.0
tags: ["api", "contract", "validation", "zod"]
</rule>
<rule>
name: shared_utilities
description: 管理前后端共享工具函数,避免代码重复
filters:
# 匹配工具函数文件
- type: file_path
pattern: "(?:utils|helpers|lib)/.*\\.(ts|js)$"
# 匹配常见工具函数内容
- type: content
pattern: "(?:export\\s+(?:default\\s+)?(?:function|const)\\s+\\w+|class\\s+\\w+)"
actions:
- type: review
criteria:
# 检查是否导入共享工具函数
- pattern: "import\\s+(?:\\{\\s*)?\\w+(?:\\s*\\})?\\s+from\\s+['\"]@shared/utils['\"]"
message: "✓ 使用共享工具函数"
not_found_message: "✗ 考虑使用@shared/utils中的共享工具函数"
optional: true
# 检查可能重复实现的工具函数
- pattern: "(?:function|const)\\s+(?:format\\w+|validate\\w+|parse\\w+|is\\w+)\\s*\\("
message: "✓ 使用共享工具函数"
not_found_message: "✗ 此功能可能已在共享工具中实现,请检查@shared/utils"
optional: true
- type: create_file
conditions:
- pattern: "src/shared/utils"
not_found: true
path: "src/shared/utils/index.ts"
content: |
/**
* 共享工具函数
*
* 可在前端和后端使用的通用工具函数集合。
*/
/**
* 日期格式化函数
* @param date - 要格式化的日期
* @param format - 格式字符串 (默认: 'YYYY-MM-DD')
* @returns 格式化后的日期字符串
*/
export const formatDate = (
date: Date | string | number,
format = 'YYYY-MM-DD'
): string => {
const d = new Date(date);
// 简单的格式化实现
const replacements: Record<string, string> = {
YYYY: d.getFullYear().toString(),
MM: (d.getMonth() + 1).toString().padStart(2, '0'),
DD: d.getDate().toString().padStart(2, '0'),
HH: d.getHours().toString().padStart(2, '0'),
mm: d.getMinutes().toString().padStart(2, '0'),
ss: d.getSeconds().toString().padStart(2, '0')
};
return Object.entries(replacements).reduce(
(result, [pattern, value]) => result.replace(pattern, value),
format
);
};
/**
* 检查字符串是否为有效的电子邮件地址
* @param email - 要验证的电子邮件地址
* @returns 是否有效
*/
export const isValidEmail = (email: string): boolean => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
};
/**
* 生成随机字符串
* @param length - 生成的字符串长度
* @returns 随机字符串
*/
export const generateRandomString = (length = 10): string => {
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
return Array.from({ length }, () =>
characters.charAt(Math.floor(Math.random() * characters.length))
).join('');
};
/**
* 深度合并对象
* @param target - 目标对象
* @param source - 源对象
* @returns 合并后的对象
*/
export const deepMerge = <T extends object, U extends object>(
target: T,
source: U
): T & U => {
const output = { ...target } as T & U;
if (isObject(source) && isObject(target)) {
Object.keys(source).forEach(key => {
if (isObject(source[key])) {
if (!(key in target)) {
Object.assign(output, { [key]: source[key] });
} else {
output[key] = deepMerge(target[key], source[key]);
}
} else {
Object.assign(output, { [key]: source[key] });
}
});
}
return output;
};
/**
* 检查值是否为对象
* @param item - 要检查的值
* @returns 是否为对象
*/
export const isObject = (item: unknown): item is Record<string, unknown> => {
return item !== null && typeof item === 'object' && !Array.isArray(item);
};
/**
* 截断文本并添加省略号
* @param text - 要截断的文本
* @param maxLength - 最大长度
* @returns 截断后的文本
*/
export const truncateText = (text: string, maxLength = 100): string => {
if (text.length <= maxLength) return text;
return `${text.substring(0, maxLength)}...`;
};
/**
* 将对象转换为查询字符串
* @param params - 查询参数对象
* @returns 查询字符串
*/
export const objectToQueryString = (params: Record<string, any>): string => {
return Object.entries(params)
.filter(([, value]) => value !== undefined && value !== null)
.map(([key, value]) => {
if (Array.isArray(value)) {
return value.map(v => `${encodeURIComponent(key)}=${encodeURIComponent(v)}`).join('&');
}
return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
})
.join('&');
};
/**
* 从URL解析查询参数
* @param url - 包含查询参数的URL
* @returns 查询参数对象
*/
export const parseQueryString = (url: string): Record<string, string> => {
const queryString = url.split('?')[1] || '';
if (!queryString) return {};
return queryString.split('&').reduce((params, param) => {
const [key, value] = param.split('=');
if (key && value) {
params[decodeURIComponent(key)] = decodeURIComponent(value);
}
return params;
}, {} as Record<string, string>);
};
- type: suggest
message: |
确保前后端一致性的最佳实践:
1. **将可重用逻辑移至共享目录**:
- 数据验证
- 日期/时间格式化
- 字符串处理
- 数据转换
2. **考虑环境差异**:
如果函数需要在不同环境中运行,使用策略模式:
```typescript
// src/shared/utils/storage.ts
interface StorageStrategy {
get(key: string): any;
set(key: string, value: any): void;
remove(key: string): void;
}
// 浏览器环境实现
const browserStorage: StorageStrategy = {
get(key: string) {
const value = localStorage.getItem(key);
return value ? JSON.parse(value) : null;
},
set(key: string, value: any) {
localStorage.setItem(key, JSON.stringify(value));
},
remove(key: string) {
localStorage.removeItem(key);
}
};
// Node.js环境实现
const nodeStorage: StorageStrategy = {
get(key: string) {
// 实现Node.js存储逻辑
},
set(key: string, value: any) {
// 实现Node.js存储逻辑
},
remove(key: string) {
// 实现Node.js存储逻辑
}
};
// 根据环境选择适当的策略
export const storage = typeof window !== 'undefined'
? browserStorage
: nodeStorage;
```
3. **标记平台特定函数**:
```typescript
/**
* @platform browser
* 返回窗口尺寸
*/
export const getWindowSize = () => {
// 仅在浏览器环境中可用
if (typeof window === 'undefined') {
throw new Error('Function only available in browser environment');
}
return {
width: window.innerWidth,
height: window.innerHeight
};
};
```
4. **使用类型断言确保类型安全**:
```typescript
/**
* @platform node
* 读取文件内容
*/
export const readFileSync = (filePath: string): string => {
// 仅在Node.js环境中可用
if (typeof process === 'undefined') {
throw new Error('Function only available in Node.js environment');
}
const fs = require('fs') as typeof import('fs');
return fs.readFileSync(filePath, 'utf8');
};
```
metadata:
priority: medium
version: 1.0.0
tags: ["shared-utils", "full-stack", "code-reuse"]
</rule>
<rule>
name: fullstack_workflow
description: 确保全栈项目开发工作流的一致性和效率
filters:
# 匹配项目配置文件
- type: file_path
pattern: "(?:package\\.json|tsconfig\\.json|webpack\\.config\\.js|vite\\.config\\.ts)"
# 匹配修改事件
- type: event
pattern: "file_modify"
actions:
- type: review
criteria:
# 检查monorepo设置
- pattern: "\"workspaces\":\\s*\\[\\s*\"packages/\\*\""
message: "✓ 使用工作区管理前后端代码"
not_found_message: "✗ 考虑使用工作区(workspaces)管理前后端代码"
optional: true
# 检查共享脚本
- pattern: "\"scripts\":\\s*\\{[^}]*\"dev(?::all)?\":[^}]*\"concurrently[^}]*"
message: "✓ 使用并行脚本运行前后端"
not_found_message: "✗ 考虑使用concurrently运行前后端开发服务器"
optional: true
# 检查TypeScript路径别名
- pattern: "\"paths\":\\s*\\{[^}]*\"@shared"
message: "✓ 使用TypeScript路径别名"
not_found_message: "✗ 应配置TypeScript路径别名便于共享代码的导入"
optional: true
- type: create_file
conditions:
- pattern: "\"scripts\":\\s*\\{[^}]*\"dev(?::all)?\":[^}]*\"concurrently[^}]*"
not_found: true
- pattern: "package\\.json"
file_path: "package.json"
not_found: false
path: "scripts/dev.js"
content: |
/**
* 开发服务器启动脚本
* 并行启动前端和后端开发服务器。
*/
const concurrently = require('concurrently');
const path = require('path');
// 定义颜色
const colors = {
client: 'cyan',
server: 'green',
shared: 'yellow'
};
// 定义命令
const commands = [
{
command: 'npm run dev:client',
name: 'client',
prefixColor: colors.client
},
{
command: 'npm run dev:server',
name: 'server',
prefixColor: colors.server
},
{
command: 'npm run watch:shared',
name: 'shared',
prefixColor: colors.shared
}
];
// 运行命令
concurrently(commands, {
prefix: 'name',
timestampFormat: 'HH:mm:ss',
killOthers: ['failure'],
restartTries: 3
}).then(
() => {
console.log('所有进程正常结束');
process.exit(0);
},
(error) => {
console.error('有进程异常退出:', error);
process.exit(1);
}
);
- type: suggest
message: |
推荐的全栈项目配置:
1. **使用工作区管理代码**:
```json
// package.json
{
"name": "my-fullstack-app",
"private": true,
"workspaces": [
"packages/*"
],
"scripts": {
"dev": "node scripts/dev.js",
"dev:client": "cd packages/client && npm run dev",
"dev:server": "cd packages/server && npm run dev",
"watch:shared": "cd packages/shared && npm run build:watch",
"build": "npm run build:shared && npm run build:client && npm run build:server",
"build:shared": "cd packages/shared && npm run build",
"build:client": "cd packages/client && npm run build",
"build:server": "cd packages/server && npm run build",
"test": "npm run test:shared && npm run test:client && npm run test:server",
"test:shared": "cd packages/shared && npm test",
"test:client": "cd packages/client && npm test",
"test:server": "cd packages/server && npm test"
},
"devDependencies": {
"concurrently": "^7.6.0"
}
}
```
2. **配置TypeScript路径别名**:
```json
// tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@shared/*": ["packages/shared/src/*"],
"@client/*": ["packages/client/src/*"],
"@server/*": ["packages/server/src/*"]
}
}
}
```
3. **使用环境变量共享配置**:
```
// .env
API_URL=http://localhost:3000/api
API_TIMEOUT=5000
APP_NAME=MyFullStackApp
```
4. **添加开发脚本到package.json**:
```json
{
"scripts": {
"dev": "node scripts/dev.js"
},
"devDependencies": {
"concurrently": "^7.6.0"
}
}
```
5. **创建共享ESLint配置**:
```js
// eslint.config.js
const sharedRules = {
"no-console": ["warn", { allow: ["warn", "error"] }],
"no-unused-vars": "warn",
"max-len": ["error", { "code": 100 }]
};
module.exports = {
shared: {
rules: sharedRules
},
client: {
extends: ["react-app"],
rules: {
...sharedRules,
"react/prop-types": "error"
}
},
server: {
extends: ["eslint:recommended", "plugin:node/recommended"],
rules: {
...sharedRules,
"node/no-unsupported-features/es-syntax": "off"
}
}
};
```
metadata:
priority: high
version: 1.0.0
tags: ["full-stack", "workflow", "monorepo"]
</rule>
团队在项目中实施这套规则后,取得了以下成效:
- 类型定义统一 - 前后端共享类型定义减少了90%的类型不匹配错误
- API接口稳定 - 使用Zod进行运行时验证,降低了API不兼容问题的发生率
- 代码复用提高 - 共享工具函数减少了30%的代码重复
- 开发效率提升 - 统一的工作流和工具配置使团队效率提高了25%
- 沟通成本降低 - 前后端开发者使用相同的术语和概念,减少了误解和沟通成本
从这个案例中,团队总结了以下最佳实践:
- "共享优先"原则 - 任何可能在前后端共用的代码都应考虑放入共享目录
- 单一真相来源 - 对于类型定义和API契约,坚持单一真相来源原则
- 渐进式采用 - 从最有价值的共享部分开始,逐步扩展共享范围
- 自动化工具 - 使用脚本和工具自动检查和同步前后端代码
- 开发体验优化 - 关注开发环境的便利性,确保前后端开发体验一致
团队也遇到了一些需要注意的问题:
- 共享代码的性能影响 - 需要确保共享代码不会导致前端包体积过大
- 环境差异处理 - 某些功能在前端和后端有不同实现,需要适当抽象
- 项目结构学习曲线 - 新团队成员需要时间适应共享代码的项目结构
一个DevOps团队负责多个项目的持续集成和部署流程,面临以下挑战:
- CI/CD流程不一致 - 不同项目使用不同的CI/CD配置和流程
- 质量检查分散 - 代码质量、安全性和性能检查分布在不同工具和脚本中
- 部署风险 - 缺乏统一的部署前检查和回滚机制
- 环境配置差异 - 开发、测试和生产环境配置不一致,导致"在我机器上能运行"问题
从前面的案例我们可以看到,Cursor规则不仅可以应用于代码编写,还可以扩展到DevOps流程中。在下一部分中,我们将详细探讨DevOps场景中的Cursor规则应用案例,特别关注自动化流程和质量把关。