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