RESTful API 路由设计规范
RESTful API 是一种基于 HTTP 协议的 API 设计风格,强调资源的概念和状态转移。Koa2 作为 Node.js 的轻量级框架,非常适合实现 RESTful API。合理的路由设计能提升 API 的可读性、可维护性和扩展性。
资源与路由映射
RESTful API 的核心是将业务模型抽象为资源,通过 HTTP 方法对资源进行操作。路由设计应直接反映资源层级关系,例如:
// 用户资源路由
router.get('/users', getUserList);
router.post('/users', createUser);
router.get('/users/:id', getUserDetail);
router.put('/users/:id', updateUser);
router.delete('/users/:id', deleteUser);
资源名称使用复数形式,避免动词出现在 URI 中。对于嵌套资源,采用父子关系表示:
// 用户的文章资源
router.get('/users/:userId/articles', getUserArticles);
router.post('/users/:userId/articles', createUserArticle);
HTTP 方法语义化
不同 HTTP 方法对应特定操作语义:
- GET:获取资源(安全且幂等)
- POST:创建资源(非幂等)
- PUT:完整更新资源(幂等)
- PATCH:部分更新资源(幂等)
- DELETE:删除资源(幂等)
错误示例:
// 错误:使用 GET 执行删除操作
router.get('/users/:id/delete', deleteUser);
正确做法应严格遵循方法语义:
router.delete('/users/:id', deleteUser);
版本控制
API 版本应体现在路由中,常见方案:
- URI 路径版本:
router.get('/v1/users', getV1Users);
router.get('/v2/users', getV2Users);
- 请求头版本:
// 通过中间件处理版本
app.use(async (ctx, next) => {
const version = ctx.get('X-API-Version') || 'v1';
ctx.state.apiVersion = version;
await next();
});
查询参数规范
GET 请求的过滤、分页、排序应通过查询参数实现:
// 分页查询示例
router.get('/articles', async ctx => {
const { page = 1, limit = 10, sort = '-createdAt' } = ctx.query;
// 处理查询逻辑
});
// 使用示例:
// GET /articles?page=2&limit=20&sort=-views,title
参数命名建议:
- 分页:
page
,limit
/per_page
- 排序:
sort=field1,-field2
(负号表示降序) - 过滤:
status=published&author=john
状态码应用
Koa2 中应正确设置 HTTP 状态码:
// 创建成功
ctx.status = 201;
// 无内容
ctx.status = 204;
// 参数错误
ctx.status = 400;
// 认证失败
ctx.status = 401;
避免所有请求都返回 200,错误详情应通过响应体传递:
ctx.status = 404;
ctx.body = {
error: 'Not Found',
message: 'User with id 123 does not exist'
};
路由组织实践
大型项目建议分模块组织路由:
// app.js
const userRouter = require('./routes/users');
const productRouter = require('./routes/products');
app.use(userRouter.routes());
app.use(productRouter.routes());
// routes/users.js
const Router = require('koa-router');
const router = new Router({ prefix: '/users' });
router.get('/', ...);
router.post('/', ...);
使用 prefix
简化路由定义,配合中间件实现统一处理:
// 认证中间件
const auth = async (ctx, next) => {
if (!ctx.state.user) ctx.throw(401);
await next();
};
router.post('/profile', auth, updateProfile);
特殊操作处理
对非标准资源操作,可采用以下方案:
- 子资源形式:
// 激活用户
router.post('/users/:id/activate', activateUser);
- 动作参数:
// 批量操作
router.post('/users/batch', batchUpdateUsers);
- 特殊端点(慎用):
// 搜索接口
router.get('/search', globalSearch);
HATEOAS 实现
超媒体作为应用状态引擎,可在响应中嵌入相关链接:
router.get('/users/:id', async ctx => {
const user = await getUser(ctx.params.id);
ctx.body = {
...user,
_links: {
self: { href: `/users/${user.id}` },
articles: { href: `/users/${user.id}/articles` }
}
};
});
性能优化技巧
- 字段过滤:
// GET /users?fields=name,email
router.get('/users', async ctx => {
const fields = ctx.query.fields ? ctx.query.fields.split(',') : null;
// 查询时只选择指定字段
});
- 压缩响应:
const compress = require('koa-compress');
app.use(compress());
- 缓存控制:
router.get('/products/:id', async ctx => {
ctx.set('Cache-Control', 'public, max-age=3600');
// ...
});
安全注意事项
- 速率限制:
const ratelimit = require('koa-ratelimit');
app.use(ratelimit({
driver: 'memory',
db: new Map(),
duration: 60000,
max: 100
}));
- CORS 配置:
const cors = require('@koa/cors');
app.use(cors({
origin: ctx => ['https://example.com'].includes(ctx.header.origin) ? ctx.header.origin : false
}));
- 输入验证:
const Joi = require('joi');
const schema = Joi.object({
username: Joi.string().alphanum().min(3).max(30).required()
});
router.post('/users', async ctx => {
const { error } = schema.validate(ctx.request.body);
if (error) ctx.throw(400, error.details[0].message);
// ...
});
文档化建议
使用 OpenAPI/Swagger 规范生成文档:
const swagger = require('koa2-swagger-ui');
const { koaSwagger } = require('koa2-swagger-ui');
app.use(swagger({
routePrefix: '/docs',
swaggerOptions: {
url: '/swagger.json'
}
}));
// 生成规范文件
const spec = {
openapi: '3.0.0',
info: {
title: 'API文档',
version: '1.0.0'
},
paths: {
'/users': {
get: {
summary: '获取用户列表',
responses: {
200: { description: '成功' }
}
}
}
}
};
router.get('/swagger.json', ctx => {
ctx.body = spec;
});
错误处理统一化
创建自定义错误类:
class ApiError extends Error {
constructor(status, code, message) {
super(message);
this.status = status;
this.code = code;
}
}
// 全局错误处理
app.use(async (ctx, next) => {
try {
await next();
} catch (err) {
ctx.status = err.status || 500;
ctx.body = {
error: err.code || 'INTERNAL_ERROR',
message: err.message
};
}
});
// 业务中使用
router.post('/login', async ctx => {
if (!ctx.request.body.password) {
throw new ApiError(400, 'INVALID_INPUT', 'Password required');
}
// ...
});
测试策略
使用 Supertest 进行路由测试:
const request = require('supertest');
const app = require('../app');
describe('User API', () => {
it('GET /users should return 200', async () => {
const res = await request(app.callback())
.get('/users')
.expect(200);
expect(Array.isArray(res.body)).toBe(true);
});
it('POST /users with invalid data should return 400', async () => {
await request(app.callback())
.post('/users')
.send({})
.expect(400);
});
});
监控与日志
添加请求日志中间件:
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
});
// 错误日志
app.on('error', (err, ctx) => {
logError(err, ctx.request);
});
部署注意事项
- 反向代理配置:
location /api/ {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
}
- 健康检查端点:
router.get('/health', ctx => {
ctx.body = { status: 'OK' };
});
- 环境变量管理:
// config.js
module.exports = {
apiPrefix: process.env.API_PREFIX || '/api/v1'
};
// app.js
const config = require('./config');
router.prefix(config.apiPrefix);
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn
下一篇:路由参数与查询参数的获取