阿里云主机折上折
  • 微信号
您当前的位置:网站首页 > RESTful API 路由设计规范

RESTful API 路由设计规范

作者:陈川 阅读数:10648人阅读 分类: Node.js

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 版本应体现在路由中,常见方案:

  1. URI 路径版本:
router.get('/v1/users', getV1Users);
router.get('/v2/users', getV2Users);
  1. 请求头版本:
// 通过中间件处理版本
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);

特殊操作处理

对非标准资源操作,可采用以下方案:

  1. 子资源形式:
// 激活用户
router.post('/users/:id/activate', activateUser);
  1. 动作参数:
// 批量操作
router.post('/users/batch', batchUpdateUsers);
  1. 特殊端点(慎用):
// 搜索接口
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` }
    }
  };
});

性能优化技巧

  1. 字段过滤:
// GET /users?fields=name,email
router.get('/users', async ctx => {
  const fields = ctx.query.fields ? ctx.query.fields.split(',') : null;
  // 查询时只选择指定字段
});
  1. 压缩响应:
const compress = require('koa-compress');
app.use(compress());
  1. 缓存控制:
router.get('/products/:id', async ctx => {
  ctx.set('Cache-Control', 'public, max-age=3600');
  // ...
});

安全注意事项

  1. 速率限制:
const ratelimit = require('koa-ratelimit');
app.use(ratelimit({
  driver: 'memory',
  db: new Map(),
  duration: 60000,
  max: 100
}));
  1. CORS 配置:
const cors = require('@koa/cors');
app.use(cors({
  origin: ctx => ['https://example.com'].includes(ctx.header.origin) ? ctx.header.origin : false
}));
  1. 输入验证:
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);
});

部署注意事项

  1. 反向代理配置:
location /api/ {
  proxy_pass http://localhost:3000;
  proxy_set_header Host $host;
}
  1. 健康检查端点:
router.get('/health', ctx => {
  ctx.body = { status: 'OK' };
});
  1. 环境变量管理:
// config.js
module.exports = {
  apiPrefix: process.env.API_PREFIX || '/api/v1'
};

// app.js
const config = require('./config');
router.prefix(config.apiPrefix);

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

前端川

前端川,陈川的代码茶馆🍵,专治各种不服的Bug退散符💻,日常贩卖秃头警告级的开发心得🛠️,附赠一行代码笑十年的摸鱼宝典🐟,偶尔掉落咖啡杯里泡开的像素级浪漫☕。‌