482 lines
12 KiB
Markdown
482 lines
12 KiB
Markdown
# 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>
|
||
);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
文档由阿瑞创建和维护。如有问题,请联系系统管理员。 |