Koa.js RESTful API进阶设计与HATEOAS实践
HATEOAS超媒体控制
HATEOAS(Hypermedia as the Engine of Application State)是 RESTful API 成熟度模型中最高级别的特性。通过在响应中包含相关链接,客户端可以动态发现和导航 API,而无需硬编码 URL。
基础 HATEOAS 实现
// HATEOAS 响应包装器
function createHateoasResponse(data, ctx, relations = {}) {
const baseUrl = `${ctx.protocol}://${ctx.host}`;
const hateoas = {
data,
_links: {
self: { href: `${baseUrl}${ctx.path}` },
...relations
}
};
// 如果是列表,添加分页链接
if (data.items && data.total !== undefined) {
hateoas._links.first = { href: `${baseUrl}${ctx.path}?page=1&size=${data.size}` };
hateoas._links.last = { href: `${baseUrl}${ctx.path}?page=${data.totalPages}&size=${data.size}` };
if (data.page > 1) {
hateoas._links.prev = { href: `${baseUrl}${ctx.path}?page=${data.page - 1}&size=${data.size}` };
}
if (data.page < data.totalPages) {
hateoas._links.next = { href: `${baseUrl}${ctx.path}?page=${data.page + 1}&size=${data.size}` };
}
}
return hateoas;
}
// 使用示例
router.get('/users', async (ctx) => {
const users = await User.findAll({ page: ctx.query.page, size: ctx.query.size });
ctx.body = createHateoasResponse(users, ctx, {
'create-user': { href: `${ctx.origin}/users`, method: 'POST' }
});
});
router.get('/users/:id', async (ctx) => {
const user = await User.findById(ctx.params.id);
ctx.body = createHateoasResponse(user, ctx, {
'update-user': { href: `${ctx.origin}/users/${user.id}`, method: 'PUT' },
'delete-user': { href: `${ctx.origin}/users/${user.id}`, method: 'DELETE' },
'user-orders': { href: `${ctx.origin}/users/${user.id}/orders`, method: 'GET' }
});
});
响应示例
{
"data": {
"id": 1,
"name": "张三",
"email": "zhangsan@example.com"
},
"_links": {
"self": { "href": "https://api.example.com/users/1" },
"update-user": { "href": "https://api.example.com/users/1", "method": "PUT" },
"delete-user": { "href": "https://api.example.com/users/1", "method": "DELETE" },
"user-orders": { "href": "https://api.example.com/users/1/orders", "method": "GET" }
}
}
高级过滤与查询
实现灵活且高效的查询接口,支持复杂的过滤、排序、分页需求。
通用查询构建器
// 查询参数解析中间件
const queryParser = async (ctx, next) => {
const { page = 1, size = 10, sort, fields, ...filters } = ctx.query;
// 解析分页
ctx.queryBuilder = {
page: parseInt(page),
size: Math.min(parseInt(size), 100), // 限制最大100条
offset: (parseInt(page) - 1) * parseInt(size)
};
// 解析排序
if (sort) {
ctx.queryBuilder.sort = sort.split(',').map(s => {
const [field, direction] = s.split(':');
return { field, direction: direction || 'asc' };
});
}
// 解析字段过滤
if (fields) {
ctx.queryBuilder.fields = fields.split(',');
}
// 解析过滤条件
ctx.queryBuilder.filters = Object.entries(filters)
.filter(([key]) => !['page', 'size', 'sort', 'fields'].includes(key))
.reduce((acc, [key, value]) => {
// 支持 operators: gt, lt, gte, lte, like, in
const [field, operator] = key.split('_');
acc.push({ field, operator: operator || 'eq', value });
return acc;
}, []);
await next();
};
// 应用查询构建器
router.get('/products', queryParser, async (ctx) => {
const products = await Product.query(ctx.queryBuilder);
ctx.body = products;
});
使用示例
| 查询参数 | 说明 | 示例 |
|---|---|---|
| page, size | 分页参数 | ?page=2&size=20 |
| sort | 排序字段 | ?sort=created_at:desc,price:asc |
| fields | 返回字段 | ?fields=id,name,price |
| price_gt | 大于过滤 | ?price_gt=100 |
| name_like | 模糊匹配 | ?name_like=手机 |
响应数据结构标准化
统一的响应格式便于前端处理,提高 API 的一致性和可预测性。
统一响应格式
// 统一响应格式中间件
const responseFormatter = async (ctx, next) => {
// 保存原始body
await next();
// 如果已经格式化,直接返回
if (ctx.body && ctx.body._formatted) return;
const originalBody = ctx.body;
// 构建统一响应
ctx.body = {
success: true,
data: originalBody,
timestamp: Date.now(),
path: ctx.path,
method: ctx.method
};
ctx.body._formatted = true;
};
响应格式示例
// 成功响应
{
"success": true,
"data": { "id": 1, "name": "产品A", "price": 99.00 },
"timestamp": 1623456789000,
"path": "/api/products/1",
"method": "GET"
}
// 列表响应(带分页)
{
"success": true,
"data": {
"items": [...],
"total": 100,
"page": 1,
"size": 20,
"totalPages": 5
},
"timestamp": 1623456789000,
"path": "/api/products",
"method": "GET"
}
// 错误响应
{
"success": false,
"error": {
"code": "INVALID_PARAMETER",
"message": "价格不能为负数"
},
"timestamp": 1623456789000,
"path": "/api/products",
"method": "POST"
}
API 版本控制策略
RESTful API 需要考虑版本管理,确保向后兼容。
URL 版本控制
// 版本控制路由
const v1Router = new Router({ prefix: '/v1' });
const v2Router = new Router({ prefix: '/v2' });
// v1 路由
v1Router.get('/users', userController.listV1);
v1Router.get('/users/:id', userController.getV1);
// v2 路由 - 添加了新字段和功能
v2Router.get('/users', userController.listV2);
v2Router.get('/users/:id', userController.getV2);
v2Router.get('/users/:id/profile', userController.getProfileV2);
app.use(v1Router.routes());
app.use(v2Router.routes());
总结
HATEOAS、过滤查询、统一响应格式和版本控制是 RESTful API 进阶设计的核心要素。合理运用这些技术可以显著提升 API 的可用性、可维护性和扩展性。