REST API 规范
REST请求只是一种请求类型和响应类型均为JSON的HTTP请求。
编写REST API,实际上就是编写处理HTTP请求的async函数,不过,REST请求和普通的HTTP请求有几个特殊的地方:
REST请求仍然是标准的HTTP请求,但是,除了GET请求外,POST、PUT等请求的body是JSON数据格式,请求的Content-Type为application/json;
REST响应返回的结果是JSON数据格式,因此,响应的Content-Type也是application/json。
REST规范定义了资源的通用访问格式,虽然它不是一个强制要求,但遵守该规范可以让人易于理解。
例如,商品Product就是一种资源:
-
GET /api/products 获取所有Product;
-
GET /api/products/123 而获取某个指定的Product ,指定id为123;
-
POST /api/products 新建一个Product,JSON数据包含在body中;
-
PUT /api/products/123 更新一个Product,更新id为123;
-
DELETE /api/products/123 删除一个Product使用DELETE请求,删除id为123;
-
GET /api/products/123/reviews 资源还可以按层次组织。如,获取某个Product的所有评论;
-
GET /api/products/123/reviews?page=2&size=10&sort=time 也可通过参数限制返回的结果集。如,返回第2页评论,每页10项,按时间排序。
REST API 编写
使用REST虽然非常简单,但是,设计一套合理的REST框架却需要仔细考虑很多问题。
问题一:如何组织URL
在实际工程中,一个Web应用既有REST,还有MVC,可能还需要集成其他第三方系统。如何组织URL?
一个简单的方法是通过固定的前缀区分。例如,/static/
开头的URL是静态资源文件,类似的,/api/
开头的URL就是REST API,其他URL是普通的MVC请求。
问题二:如何统一输出REST
-
REST API的返回值全部是object对象。
为方便客户端处理结果,只要是请求成功,不管是查询错误还是查询异常,都返回JSON数据,而不是简单的 number、boolean、null或者数组; -
REST API必须使用前缀/api/
如果需要对请求做某些统一处理,就可以通过/api/
来判断当前请求是否是一个 REST 请求。
问题三:如何处理错误
这个问题实际上有两部分。
HTTP请求可能发生的错误
当REST API请求出错时,像403,404,500等错误,我们如何返回错误信息?
针对这种类型的错误,第一类的错误实际上客户端可以识别,并且我们也无法操控HTTP服务器的错误码。
业务逻辑的错误
当客户端收到 REST 响应后,如何判断是成功还是错误?
例如,输入了不合法的Email地址,试图删除一个不存在的Product,等等。这种类型的错误完全可以通过JSON返回给客户端,这样,客户端可以根据错误信息提示用户“Email不合法”等,以便用户修复后重新请求API。例如:
{
"code": "0",
"message": "Bad email address"
}
REST架构本身同样没有标准的错误码定义一说,因此,有的Web应用使用数字1000、1001……作为错误码。
不推荐混合其他HTTP错误码。例如,使用401响应“登录失败”,使用403响应“权限不够”。这会使客户端无法有效识别HTTP错误码和业务错误,其原因在于HTTP协议定义的错误码十分偏向底层,而REST API属于“高层”协议,不应该复用底层的错误码。
koa 处理 REST
使用koa作为Web框架处理HTTP请求,因此,我们可以在koa中响应并处理REST请求。
我来看下koa一个简单的路由请求案例:
const Koa = require('koa')
const router = require('koa-router')() // 处理路由映射
const bodyParser = require('koa-bodyparser') // 处理提交请求
const app = new Koa()
// add bodyparser
app.use(bodyParser())
// add url-route:
router.get('/', async (ctx, next) => {
ctx.response.body = '<h1>Hello, koa2!!!</h1>'
});
router.get('/hello/:name', async (ctx, next) => {
var name = ctx.params.name;
ctx.response.body = `<h1>Hello, ${name}!</h1>`
});
router.get('/login', async (ctx, next) => {
ctx.response.body = `
<h1>Index</h1>
<form action="/signin" method="post">
<p>Name: <input name="name" value="koa"></p>
<p>Password: <input name="password" type="password"></p>
<p><input type="submit" value="Submit"></p>
</form>`
});
router.post('/signin', async (ctx, next) => {
const { name, password } = ctx.request.body
if (name === 'koa' && password === '123456') {
const result = {
message: 'login success',
username: name
}
ctx.response.body = result
} else {
ctx.response.body = `<h1>Login failed!</h1>`
}
})
// add router middleware:
app.use(router.routes());
app.listen(3000);
console.log('app started at port 3000...');
上面代码中有两个关键的 middleware:koa-router
和 koa-bodyparser
koa-router
为处理URL,我们需要引入koa-router这个middleware,让它负责处理URL映射,根据不同的URL调用不同的处理函数。
安装、引入。
然后就使用 router.get('/path', async fn)
来注册一个GET请求。如果API路径带有参数,参数必须用 :
表示,参数通过 ctx.params.
来访问。
例如上面例子中的 /hello/:name
,参数通过 ctx.params.name
访问。
客户端传递的URL可能就是 /hello/007
,那么参数 name
对应的值就是 007
,用 ctx.params.name
来获取。
类似的,如果API路径有多个参数,例如,/api/products/:pid/reviews/:rid
,则这两个参数分别用
ctx.params.pid
和 ctx.params.rid
获取。
这个功能由 koa-router
这个middleware提供。
注意:API路径的参数永远是字符串!
koa-bodyparser
如果要处理post请求,可以用 router.post('/path', async fn)
。
用post请求处理URL时,我们会遇到一个问题:post请求通常会发送一个表单,或者JSON,它作为request的body发送,但无论是Node.js提供的原始request对象,还是koa提供的request对象,都不提供解析request的body的功能!
所以,我们需要引入另一个middleware来解析原始request请求,然后,把解析后的参数,绑定到 ctx.request.body 中。
koa-bodyparser
就是用来干这个活的。
注意,由于middleware的顺序很重要,这个 koa-bodyparser
必须在router之前被注册到app对象上。如果 ctx.request.body 为 undefined,说明缺少middleware,或者middleware没有正确配置。
koa-bodyparser
给 koa 安装了一个解析HTTP请求body的处理函数。
如果客户端传递了JSON格式的数据(例如,POST、PUT等请求),就可以通过 ctx.request.body
直接访问已经反序列化的 JavaScript 对象。
如果要返回JSON格式的数据到客户端,只需要给 ctx.response.body
赋值一个JavaScript对象,koa会自动把该对象序列化为JSON并输出到客户端。
响应 response.type
在 koa内部,koa 会根据 ctx.response.body
响应体容格式的不同设置默认的Content-Type。
将响应体设置为以下之一 并 赋 Content-Type 默认值:
-
string 写入
Content-Type 默认为 text/html 或 text/plain, 同时默认字符集是 utf-8。Content-Length 字段也是如此。 -
Buffer 写入
Content-Type 默认为 application/octet-stream, 并且 Content-Length 字段也是如此。 -
Stream 管道
Content-Type 默认为 application/octet-stream -
Object || Array JSON 字符串化
Content-Type 默认为 application/json. 这包括普通的对象 { foo: 'bar' } 和数组 ['foo', 'bar'] -
null 无内容响应
Content-Type ,无
我们也可以通过 response.type 自行设置 Content-Type:
【 获取】
获取响应 Content-Type, 获取到的值不含 "charset" 等参数:
const ct = ctx.type // => "image/png"
【 设置】
设置响应 Content-Type :
ctx.type = 'text/plain; charset=utf-8';
ctx.type = 'image/png';
ctx.type = '.png';
ctx.type = 'png';
如果不设置字符串charset,将默认是 "utf-8"。
如果你想覆盖 charset, 使用 ctx.set('Content-Type', 'text/html')
将响应头字段设置为直接值。
例如上面例中的get请求:
router.get('/', async (ctx, next) => {
ctx.response.type = 'text/html'; // 'text/html; charset=utf-8'
/*或*/
ctx.set('Content-Type', 'text/html') // 'text/html'
ctx.response.body = '<h1>Hello, koa2!!!</h1>'
});
网友评论