SAAS/README-API.md

482 lines
12 KiB
Markdown
Raw Permalink Normal View History

2025-05-14 00:30:11 +08:00
# API请求规范与最佳实践
## 目录
- [基本规范](#基本规范)
- [响应格式](#响应格式)
- [错误处理](#错误处理)
- [参数验证](#参数验证)
- [安全性考虑](#安全性考虑)
- [API路由实现](#api路由实现)
- [基础API路由](#基础api路由)
- [动态路由实现](#动态路由实现)
- [Next.js 15.3+路由处理器](#nextjs-153路由处理器)
- [最佳实践](#最佳实践)
- [常见问题](#常见问题)
## 基本规范
### 响应格式
所有API响应应遵循统一的格式
```typescript
// 成功响应
{
success: true,
data?: any, // 通用数据字段
[key: string]: any, // 或使用特定业务字段名如users, products等
message?: string // 可选,成功消息
}
// 错误响应
{
success: false,
error: string, // 错误消息
code?: string // 可选,错误代码
}
```
**示例:通用数据字段**
```typescript
return NextResponse.json({
success: true,
data: rows
});
```
**示例:特定业务字段**
```typescript
return NextResponse.json({
success: true,
users: rows // 使用更具描述性的字段名
});
```
### 错误处理
始终使用try-catch处理API操作并返回适当的错误信息
```typescript
try {
// API操作
} catch (error) {
console.error('操作描述失败:', error);
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : '未知错误'
},
{ status: 500 }
);
}
```
### 参数验证
在执行操作前验证输入参数:
```typescript
// 获取参数
const { id } = params;
// 验证参数
if (!id || isNaN(Number(id))) {
return NextResponse.json(
{ success: false, error: '无效的ID参数' },
{ status: 400 }
);
}
// 继续操作
```
### 安全性考虑
- 使用参数化查询防止SQL注入
- 不要在响应中返回敏感信息(如密码、令牌)
- 对数据库操作使用最小权限原则
- 验证用户权限和身份认证信息
```typescript
// 良好实践 - 使用参数化查询
const [user] = await req.db.query(
'SELECT id, username FROM users WHERE id = ?',
[userId]
);
// 不良实践 - 容易遭受SQL注入
const [user] = await req.db.query(
`SELECT * FROM users WHERE id = ${userId}`
);
```
## API路由实现
### 基础API路由
使用数据库连接中间件的API路由基本实现
```typescript
// API路由文件app/api/your-route/route.ts
import { NextResponse } from 'next/server';
import { connectSystemDB, RequestWithDB } from '@/lib/db';
async function handler(req: RequestWithDB) {
try {
// 使用req.db访问数据库连接
const [rows] = await req.db.query('SELECT * FROM your_table');
return NextResponse.json({
success: true,
data: rows
});
} catch (error) {
console.error('操作失败:', error);
return NextResponse.json(
{ success: false, error: '数据库操作失败' },
{ status: 500 }
);
}
}
// 导出使用中间件包装的处理函数
export const GET = connectSystemDB(handler);
```
### 必要的导入说明
在Next.js的API路由中通常需要以下两个核心导入
```typescript
import { NextRequest, NextResponse } from 'next/server';
```
- **NextRequest**: 扩展了原生Request对象提供了额外的便利方法和属性
- `nextUrl`: 获取解析后的URL对象可访问`pathname``searchParams`
- `cookies`: 访问和操作请求的Cookie
- `headers`: 获取请求头
- `json()`: 解析JSON请求体
- **NextResponse**: 用于创建和返回响应,提供了多种便利方法:
- `NextResponse.json()`: 创建JSON响应自动设置Content-Type
- `NextResponse.redirect()`: 创建重定向响应
- `NextResponse.rewrite()`: 创建重写响应
- `NextResponse.next()`: 继续请求处理链
这两个对象是构建API路由的基础几乎在所有API路由实现中都需要使用。
### 动态路由实现
在Next.js的动态路由中必须**正确处理动态参数**。在13.4以后的版本中,动态路由参数需要使用`await`来获取:
```typescript
// 正确的动态路由实现
// API路由文件app/api/your-route/[param]/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { connectSystemDB, RequestWithDB } from '@/lib/db';
export const GET = async (
req: NextRequest,
context: { params: { param: string } }
) => {
try {
// 正确获取动态参数 - 使用await
const params = await context.params;
const param = params.param;
// 验证参数
if (!param) {
return NextResponse.json(
{ success: false, error: '无效的参数' },
{ status: 400 }
);
}
// 处理函数
const handler = async (dbReq: RequestWithDB) => {
// 数据库操作
const [rows] = await dbReq.db.query(
'SELECT * FROM table WHERE id = ?',
[param]
);
return NextResponse.json({
success: true,
data: rows
});
};
// 执行处理函数
return await connectSystemDB(handler)(req);
} catch (error) {
console.error('操作失败:', error);
return NextResponse.json(
{ success: false, error: '操作失败' },
{ status: 500 }
);
}
};
```
**注意事项**
1. 使用箭头函数定义路由处理器:`export const GET = async (...) => {...}`
2. 正确获取动态参数:`const params = await context.params;`
3. 使用ES6解构后再使用参数`const param = params.param;`
4. 不要直接解构:`const { param } = context.params;` - 这会导致错误
#### 路由处理函数参数说明
在Next.js App Router中路由处理函数如GET、POST、PUT、DELETE等需要接收两个参数
1. **第一个参数**`request: NextRequest` - 包含请求信息的对象包括请求头、URL、搜索参数等
2. **第二个参数**`context: { params: { [key: string]: string } }` - 包含动态路由参数的上下文对象
对于动态路由如`/api/users/[id]`,路由处理函数会接收如下参数:
```typescript
export async function GET(
request: NextRequest,
context: { params: { id: string } }
) {
const userId = context.params.id;
// 使用userId处理请求...
}
```
这种双参数模式对于获取动态路由参数非常有用但在Next.js 15.3+版本中已被弃用转而使用单参数模式从URL中提取参数见下文
### Next.js 15.3+路由处理器
在Next.js 15.3+版本中路由处理器的类型定义发生了变化。为避免类型错误应使用以下方式实现API路由
#### 单参数路由处理器模式
对于动态路由应使用单参数函数并从URL中提取路径参数避免使用带context的双参数函数
```typescript
/**
* 符合Next.js 15.3+的路由处理器签名
*/
export async function GET(request: NextRequest) {
try {
// 从URL路径中提取动态参数
const pathname = request.nextUrl.pathname;
const parts = pathname.split('/');
const paramValue = parts[3]; // 例如: /api/path/[param]
// 验证参数
if (!paramValue) {
return NextResponse.json(
{ success: false, error: '参数不能为空' },
{ status: 400 }
);
}
// 处理逻辑
// ...
} catch (error) {
// 错误处理
}
}
```
#### 路由处理器函数声明方式
推荐使用函数声明而非箭头函数:
```typescript
// 推荐 ✅
export async function GET(request: NextRequest) {
// 处理逻辑
}
// 不推荐 ❌
export const GET = async (request: NextRequest) => {
// 处理逻辑
};
```
#### 修复类型错误示例
如果构建时出现以下类型错误:
```
Type '{ __tag__: "GET"; __param_position__: "second"; __param_type__: { params: { paramName: string; }; }; }'
does not satisfy the constraint 'ParamCheck<RouteContext>'.
```
应将路由函数从:
```typescript
// 可能导致类型错误的实现 ❌
export async function GET(
request: NextRequest,
{ params }: { params: { paramName: string } }
) {
const value = params.paramName;
// ...
}
```
修改为:
```typescript
// 修复后的实现 ✅
export async function GET(request: NextRequest) {
// 从URL中提取参数
const pathname = request.nextUrl.pathname;
const paramName = pathname.split('/')[3]; // 适当调整索引位置
// ...继续处理
}
```
## 最佳实践
1. **使用标准格式**:始终使用统一的响应格式
2. **错误处理**所有API路由都应包含try-catch块
3. **参数验证**:在处理前验证所有输入参数
4. **日志记录**:记录关键操作和错误信息
5. **权限验证**:确保用户有权限执行请求的操作
6. **状态码使用**使用正确的HTTP状态码
- 200: 成功
- 201: 创建成功
- 400: 请求错误/参数无效
- 401: 未授权
- 403: 禁止访问
- 404: 资源不存在
- 500: 服务器内部错误
7. **客户端组件中使用useSearchParams**始终将使用useSearchParams的组件包裹在Suspense边界中避免构建警告
## 常见问题
### 问:如何处理文件上传?
**答**在Next.js的App Router中使用`formData`来处理文件上传:
```typescript
export const POST = async (req: NextRequest) => {
try {
const formData = await req.formData();
const file = formData.get('file') as File;
// 处理文件上传
// ...
return NextResponse.json({ success: true });
} catch (error) {
return NextResponse.json(
{ success: false, error: '文件上传失败' },
{ status: 500 }
);
}
};
```
### 问如何实现分页API
**答**:使用查询参数实现分页:
```typescript
export const GET = async (req: NextRequest) => {
try {
const { searchParams } = new URL(req.url);
const page = parseInt(searchParams.get('page') || '1');
const limit = parseInt(searchParams.get('limit') || '10');
const offset = (page - 1) * limit;
// 查询数据
// ...
return NextResponse.json({
success: true,
data: rows,
pagination: {
page,
limit,
total: totalCount
}
});
} catch (error) {
// 错误处理
}
};
```
### 问:如何处理动态路由中的错误?
**答**确保正确使用await获取参数并使用try-catch处理可能的错误
```typescript
try {
const params = await context.params;
// 使用params...
} catch (error) {
console.error('参数获取失败:', error);
return NextResponse.json(
{ success: false, error: '参数处理错误' },
{ status: 400 }
);
}
```
### 问如何修复Next.js 15.3+中的路由处理器类型错误?
**答**使用单参数路由处理器从请求URL中提取路径参数
```typescript
export async function GET(request: NextRequest) {
try {
// 从URL路径中提取动态参数
const pathname = request.nextUrl.pathname;
const paramValue = pathname.split('/')[3]; // 根据路径结构调整索引
// 继续处理...
} catch (error) {
// 错误处理
}
}
```
### 问如何处理useSearchParams导致的构建警告
**答**将使用useSearchParams的组件包裹在Suspense边界中
```tsx
// 正确处理useSearchParams的方式
'use client';
import { Suspense } from 'react';
import { useSearchParams } from 'next/navigation';
// 创建一个独立的组件处理搜索参数
function SearchParamsHandler({ onParamsChange }) {
const searchParams = useSearchParams();
// 使用searchParams的逻辑
// ...
return null;
}
export default function Page() {
// 页面主要内容
return (
<div>
{/* 包裹在Suspense中 */}
<Suspense fallback={null}>
<SearchParamsHandler onParamsChange={handleParamsChange} />
</Suspense>
{/* 页面其他内容 */}
</div>
);
}
```
---
文档由阿瑞创建和维护。如有问题,请联系系统管理员。