不是RESTful不好,是你姿势有问题
文章来源:https://ningyu1.github.io/site/post/01-restful-design-specifications/
一、 摘要(Abstract)
RESTful API 已经非常成熟,也得到了大家的认可。我们按照 Richardson Maturity Model 对 REST 评价的模型,规范基于 level2 来设计
二、版本(Versioning)
API的版本号放入URL。例如:
https://api.jiuyescm.com/v1/
https://api.jiuyescm.com/v1.2/
三、资源、路径(Endpoint)
路径,API的具体地址。在REST中,每个地址都代表一个具体的资源(Resource
)约定如下:
- 路径仅表示资源的路径(位置),尽量不要有actions操作(一些特殊的
actions
操作除外) - 路径以 复数(名词) 进行命名资源,不管返回单个或者多个资源。
- 使用 小写字母、数字以及下划线(“_”) 。(下划线是为了区分多个单词,如user_name)
资源的路径从父到子依次如:
/{resource}/{resource_id}/{sub_resource}/{sub_resource_id}/{sub_resource_property}
使用
?
来进行资源的过滤、搜索以及分页等使用版本号,且版本号在资源路径之前
优先使用内容协商来区分表述格式,而不是使用后缀来区分表述格式
应该放在一个专用的域名下,如:
http://api.jiuyescm.com
使用SSL
综上,一个API路径可能会是
https://api.domain.com/v1/{resource}/{resource_id}/{sub_resource}/{sub_resource_id}/{sub_resource_property}
https://api.domain.com /v1/{resource}?page=1&page_size=10
https://api.domain.com /v1/{resource}?name=xx&sortby=name&order=asc
四、操作(HTTP Actions)
用HTTP
动词(方法)表示对资源的具体操作。常用的HTTP
动词有:
GET(SELECT):从服务器取出资源(一项或多项)
POST(CREATE):在服务器新建一个资源
PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)
PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性)
DELETE(DELETE):从服务器删除资源
还有两个不常用的HTTP动词
HEAD:获取资源的元数据
OPTIONS:获取信息,关于资源的哪些属性是客户端可以改变的
下面是一些例子
GET /users:列出所有用户
POST /users:新建一个用户
GET /users/{user_id}:获取某个指定用户的信息
PUT /users/{user_id}:更新某个指定用户的信息(提供该用户的全部信息)
PATCH /users/{user_id}:更新某个指定用户的信息(提供该用户的部分信息)
DELETE /users/{user_id}:删除某个用户
GET /users/{user_id}/resources:列出某个指定用户的所有权限资源
DELETE /users/{user_id}/resources/{resources_id}:删除某个指定用户的指定权限资源
五、数据(Data Format)
数据是对资源的具体描述,分为请求数据和返回数据。约定如下:
- 查询,过滤条件使用query string,例如user?name=xxx
- Content body 仅仅用来传输数据
- 通过Content-Type指定请求与返回的数据格式。其中请求数据还要指定Accept。(我们暂时只使用Json)
- 数据应该拿来就能用,不应该还要进行转换操作
- 使用字符串(YYYY-MM-dd hh:mm:ss)格式表达时间字段,例如: 2017-02-20 16:00:00
- 数据采用UTF-8编码
- 返回的数据应该尽量简单,响应状态应该包含在响应头中
- 使用 小写字母、数字以及下划线(“_”) 描述字段,不使用大写描述字段(这个由于使用了一些开源的jar所以这个不强求,比如说pageinfo我们无法修改属性名称)
- 建议资源中的唯一标识命名为id(这个不强求,有的唯一标识名称确实比较复杂)
- 属性和字符串值必须使用双引号””(这个json转换默认规则)
- 建议对每个字段设置默认值(数组型可设置为[],字符串型可设置为””,数值可设置为0,对象可设置为{}),这一条是为了方便前端/客户端进行判断字段存不存在操作(这样json转换会自动转成相应的字符)
- POST操作应该返回新建的资源;PUT/PATCH操作返回更新后的完整的资源;DELETE返回一个空文档;GET返回资源数组或当个资源
- 为了方便以后的扩展兼容,如果返回的是数组,强烈建议用一个包含如items属性的对象进行包裹,如:
{"items":[{},{}]}
示例:
POST https://api.domain.com/v1/users
Request
headers:
Accept: application/json
Content-Type: application/json;charset=UTF-8
body:
{
"user_name": "ZhangSan",
"address": "ujfhysdfsdf",
"nick": "ZS"
} Response
status: 201 Created
headers:
Content-Type: application/json;charset=UTF-8
body:
{
"requestId": sdfsdflkjoiusdf,
"code": "",
"message": "",
"items":
{
"id":"111",
"user_name": "HingKwan",
"address": "ujfhysdfsdf",
"nick": "ZS"
}
}
六、安全(Security)
调用限制
为了避免请求泛滥,给API设置速度限制很重要。入速度设置之后,可以在HTTP返回头上对返回的信息进行说明,下面是几个必须的返回头(依照twitter的命名规则)
X-Rate-Limit-Limit :当前时间段允许的并发请求数
X-Rate-Limit-Remaining:当前时间段保留的请求数
X-Rate-Limit-Reset:当前时间段剩余秒数
这个我们一般会在getway中实现
授权校验
RESTful API是无状态的也就是说用户请求的鉴权和cookie以及session无关,每一次请求都应该包含鉴权证明。 可以使用http请求头Authorization设置授权码; 必须使用User-Agent设置客户端信息, 无User-Agent请求头的请求应该被拒绝访问。具体的授权可以采用OAuth2,或者自己定义并实现相关的授权验证机制(基于token)。 这个我们一般会在getway中实现
错误
当API返回非2XX的HTTP响应时,应该采用统一的响应信息,格式如:
HTTP/1.1 400 Bad Request
Content-Type: application/json;charset=UTF-8
{
"code":"INVALID_ARGUMENT",
"message":"{error message}",
"request_id":"sdfsdfo8lkjsdf",
"items":[],
}
- HTTP Header Code:符合HTTP响应的状态码。详细见以下的“状态码”节
- code:用来表示某类错误不是具体错误,比如缺少参数等。是对HTTP Header Code的补充,开发团队可以根据自己的需要自己定义
- message:错误信息的摘要,应该是对用户处理错误有用的信息
- request_id:请求的id,方便开发定位发生错误的请求(可选)
- code的定义约定:
- 采用 大写字母命名,字母与字母之间用下划线(”_”) 隔开
- code应该用来定义错误类别,而非定义具体的某个错误。
- 缺少参数使用:MISSING_X
- 无效参数使用:INVALID_X
- 逻辑验证错误使用:VALIDATION_X
- 不存在使用:NO_FOUND_X
七、状态码(Status Codes)
服务器向用户返回的状态码和提示信息,常见的有以下一些(方括号中是该状态码对应的HTTP动词)。
200 OK - [GET/PUT/PATCH/DELETE]:服务器成功返回用户请求的数据,该操作是幂等的(Idempotent)。
201 Created - [POST/PUT/PATCH]:用户新建或修改数据成功。
202 Accepted - [*]:表示一个请求已经进入后台排队(异步任务)
204 No Content - [DELETE]:用户删除数据成功。
304 Not Modified - HTTP缓存有效。
400 Invalid Request - [POST/PUT/PATCH]:用户发出的请求有错误,服务器没有进行新建或修改数据的操作,该操作是幂等的。
401 Unauthorized - [*]:表示用户没有权限(令牌、用户名、密码错误)。
403 Forbidden - [*] 表示用户得到授权(与401错误相对),但是访问是被禁止的。
404 Not Found - [*]:用户发出的请求针对的是不存在的记录,服务器没有进行操作,该操作是幂等的。
405 Method Not Allowed - [*]:该http方法不被允许。
406 Not Acceptable - [GET]:用户请求的格式不可得(比如用户请求JSON格式,但是只有XML格式)。
410 Gone -[GET]:用户请求的资源被永久删除,且不会再得到的。
415 Unsupported Media Type - [*]:请求类型错误。
422 Unprocesable Entity - [POST/PUT/PATCH] 当创建一个对象时,发生一个验证错误。
429 Too Many Request - [*]:请求过多。
500 Internal Server Error - [*]:服务器发生错误,用户将无法判断发出的请求是否成功。
503 Service Unavailable - [*]:服务当前无法处理请求。
八、异常规范(Exceptions)
- Controller中try catch住service的异常,再转换为restful中需要抛出的异常
try {
Long id = userService.save(vo);
vo.setId(id);
} catch(BizException e) {
throw new UnprocesableEntityException(ErrorCode.USER_NAME_EXIST.getCode(), ErrorCode.USER_NAME_EXIST.getMessage());
}
- Controller中抛出的异常必须使用spring-mvc-rest包中的异常类,不允许自定义异常,选择需要返回的httpStatus对应的异常
#403 [*]:表示得到授权(与401错误相对),但是访问是被禁止的。
com.jiuyescm.spring.mvc.rest.exception.ForbiddenException #401 [GET]:用户请求的资源被永久删除,且不会再得到的。
com.jiuyescm.spring.mvc.rest.exception.GoneException #400 [POST/PUT/PATCH]:用户发出的请求有错误(常用在请求必要的参数错误上),服务器没有进行新建或修改数据的操作,该操作是幂等的。
com.jiuyescm.spring.mvc.rest.exception.InvalidRequestException #406 [GET]:用户请求的格式不可得(比如用户请求JSON格式,但是只有XML格式)或(请求参数需要数字,用户传入字符串)
com.jiuyescm.spring.mvc.rest.exception.NotAcceptableException #404 [*]:用户发出的请求针对的是不存在的记录,服务器没有进行操作,该操作是幂等的。
com.jiuyescm.spring.mvc.rest.exception.NotFoundException #401 [*]:表示没有权限(令牌、用户名、密码错误,或任何资源没有权限)
com.jiuyescm.spring.mvc.rest.exception.UnauthorizedException #422 [POST/PUT/PATCH] 当创建一个对象时,发生一个验证错误。
com.jiuyescm.spring.mvc.rest.exception.UnprocesableEntityException
- 抛出的异常中需要传入异常编码和异常信息,异常编码定义遵循上面 《安全中错误编码规范》
"MESSING_ID", "缺少参数:id"
"MESSING_NAME", "缺少参数:name"
"MESSING_ADDRESS", "缺少参数:address"
"USER_NAME_EXIST", "用户名已存在"
"USER_NOT_FOUND", "用户名不存在"
- 常用的错误编码、异常、httpStatus对应关系
"MESSING_ID", "缺少参数:id"、InvalidRequestException、400
"MESSING_NAME", "缺少参数:name"、InvalidRequestException、400
"MESSING_ADDRESS", "缺少参数:address"、InvalidRequestException、400
"USER_NAME_EXIST", "用户名已存在"、UnprocesableEntityException、422
"USER_NOT_FOUND", "用户名不存在"、NotFoundException、404
九、示例(Example)
采用user提供的示例代码
POST /users
Resource POST /v1/users
POST Parameters Endpoint requires:
Name | Type | Description |
---|---|---|
name | String | 用户名称 |
address | String | 用户住址 |
and accepts a few other parameters listed below.
Name | Type | Description |
---|---|---|
remark | String | 描述信息 |
Example
{
"name":"tuyir",
"address":"sdflkjsdf",
"remark":"sdfoiu"
}
Response Status-Code: 201 Created
{
"code": "",
"message": null,
"items": {
"id": 27,
"name": "tuyir",
"address": "sdflkjsdf",
"remark": "sdfoiu"
}
}
Name | Type | Description |
---|---|---|
code | String | 错误编码 |
message | String | 错误描述 |
Items | Objec | t 返回结果 |
id | Long | 唯一标识 |
name | String | 用户名称 |
address | String | 家庭住址 |
remark | String | 描述信息 |
Error response Status-Code: 400 Bad Request
{
"code": "MESSING_NAME",
"message": “缺少参数:name”,
"items": {}
}
Name | Type | Description |
---|---|---|
code | String | 错误编码 |
message | String | 错误描述 |
Items | Object | 返回结果 |
HTTP Error Codes
HTTP Status | Code | Description |
---|---|---|
400 | MESSING_NAME | 缺少参数:name |
400 | MESSING_ADDRESS | 缺少参数:address |
422 | USER_NAME_EXIST | 用户名已存在 |
500 | INTERNAL_SERVER_ERROR | 未知的错误 |
DELETE /users/{user_id}
Resource DELETE /v1/users/{user_id}
Path Parameters
Name | Type | Description |
---|---|---|
user_id | Long | 用户唯一标识 |
Query Parameters None
Example Request
curl –H ‘Content-Type: application/json’\
-X DELETE \
‘https://api.jiuyescm.com/v1/users/111’
Response Status-Code: 204 No Content
HTTP Error Codes
HTTP Status | Code | Description |
---|---|---|
400 | MESSING_ID | 缺少参数:id |
404 | USER_NOT_FOUND | 用户不存在 |
PUT /users
Resource PUT /v1/users
PUT Body Parameters Endpoint requires:
Name | Type | Description |
---|---|---|
user_id | Long | 用户唯一标识 |
user_name | String | 用户名称 |
address | String | 用户住址 |
and accepts a few other parameters listed below..
Name | Type | Description |
---|---|---|
remark | String | 描述信息 |
Example
{
"user_id": 12,
"name":"tuyir",
"address":"sdflkjsdf",
"remark":"sdfoiu"
}
Response Status-Code: 200 OK
{
"code": "",
"message": null,
"items": {
"id": 12,
"name": "tuyir",
"address": "sdflkjsdf",
"remark": "sdfoiu"
}
}
Name | Type | Description |
---|---|---|
code | String | 错误编码 |
message | String | 错误描述 |
Items | Object | 返回结果 |
id | Long | 唯一标识 |
name | String | 用户名称 |
address | String | 家庭住址 |
remark | String | 描述信息 |
Error response Status-Code: 400 Bad Request
{
"code": "MESSING_NAME",
"message": “缺少参数:name”,
"items": {}
}
Name | Type | Description |
---|---|---|
code | String | 错误编码 |
message | String | 错误描述 |
HTTP Error Codes
HTTP Status | Code | Description |
---|---|---|
code | String | 错误编码 |
400 | MESSING_ID | 缺少参数:id |
400 | MESSING_NAME | 缺少参数:name |
400 | MESSING_ADDRESS | 缺少参数:address |
422 | USER_NAME_EXIST | 用户名已存在 |
500 | INTERNAL_SERVER_ERROR | 未知的错误 |
GET /users/{user_id}
Resource GET /v1/users/{user_id}
Path Parameters
Name | Type | Description |
---|---|---|
user_id | Long | 用户唯一标识 |
Example Request
Curl –H 'Content-Type: application/json' \
'https://api.jiuyescm.com/v1/users/12'
Response Status-Code: 200 OK
{
"code": "",
"message": null,
"items": {
"id": 12,
"name": "tuyir",
"address": "sdflkjsdf",
"remark": "sdfoiu"
}
}
Name | Type | Description |
---|---|---|
code | String | 错误编码 |
message | String | 错误描述 |
Items | Object | 返回结果 |
id | String | 唯一标识 |
name | String | 用户名称 |
address | String | 家庭住址 |
remark | String | 描述信息 |
Error response Status-Code: 404 Bad Request
{
"code": " USER_NOT_FOUND",
"message": “用户不存在”,
"items": {}
}
Name | Type | Description |
---|---|---|
code | String | 错误编码 |
message | String | 错误描述 |
HTTP Error Codes
HTTP Status | Code | Description |
---|---|---|
400 | MESSING_ID | 缺少参数:id |
404 | USER_NOT_FOUND | 用户不存在 |
GET /users
Resource GET /v1/users
Query Parameters
Name | Type | Description |
---|---|---|
name | String | 根据用户名称进行查询 |
page | int | 第几页,不传入默认1 |
page_size | int | 每页返回多少条结果,不传入默认20 |
Example Request
Curl –H 'Content-Type: application/json' \
'https://api.jiuyescm.com/v1/users?name=xxx&page=1&page_size=20'
Response Status-Code: 200 Success
{
"code": "",
"message": "",
"items": {
"pageNum": 1,
"pageSize": 20,
"size": 17,
"startRow": 1,
"endRow": 17,
"total": 17,
"pages": 1,
"list": [
{
"id": 2,
"name": "ningyu1",
"address": "sdflkjsdf",
"remark": "sdfoiu"
},
{
"id": 3,
"name": "ningyu2",
"address": "sdflkjsdf",
"remark": "sdfoiu"
},
{
"id": 4,
"name": "ningyu3",
"address": "sdflkjsdf",
"remark": "sdfoiu"
},
{
"id": 5,
"name": "ningyu4",
"address": "sdflkjsdf",
"remark": "sdfoiu"
},
{
"id": 6,
"name": "ningyu5",
"address": "sdflkjsdf",
"remark": "sdfoiu"
},
{
"id": 7,
"name": "8888",
"address": "8888",
"remark": "8888"
},
{
"id": 8,
"name": "444",
"address": "444",
"remark": "444"
},
{
"id": 9,
"name": "ningyu7",
"address": "sdflkjsdf",
"remark": "sdfoiu"
},
{
"id": 12,
"name": "ningyu9",
"address": "sdflkjsdf",
"remark": "sdfoiu"
},
{
"id": 13,
"name": "ningyu10",
"address": "sdflkjsdf",
"remark": "sdfoiu"
},
{
"id": 17,
"name": "ningyu",
"address": "sdflkjsdf",
"remark": null
},
{
"id": 20,
"name": "9999",
"address": "sdflkjsdf",
"remark": null
},
{
"id": 23,
"name": "888",
"address": "sdflkjsdf",
"remark": "sdfoiu"
},
{
"id": 24,
"name": "222",
"address": "sdflkjsdf",
"remark": "sdfoiu"
},
{
"id": 25,
"name": "222444",
"address": "sdflkjsdf",
"remark": "sdfoiu"
},
{
"id": 26,
"name": "222444sdf",
"address": "sdflkjsdf",
"remark": "sdfoiu"
},
{
"id": 27,
"name": "tuyir",
"address": "sdflkjsdf",
"remark": "sdfoiu"
}
],
"firstPage": 1,
"prePage": 0,
"nextPage": 0,
"lastPage": 1,
"isFirstPage": true,
"isLastPage": true,
"hasPreviousPage": false,
"hasNextPage": false,
"navigatePages": 8,
"navigatepageNums": [
1
]
}
}
Name | Type | Description |
---|---|---|
code | String | 错误编码 |
message | String | 错误描述 |
Items | Object | 返回结果 |
id | Long | 唯一标识 |
name | String | 用户名称 |
address | String | 家庭住址 |
remark | String | 描述信息 |
不是RESTful不好,是你姿势有问题的更多相关文章
- ABP Framework 为什么好上手,不好深入?探讨最佳学习姿势!
离写上一篇经验总结 ABP Framework 研习社经验总结(6.28-7.2) ,已经过去两周. ABP Framework 研习社(QQ群:726299208) 最近一周,又迎来了很多新伙伴,成 ...
- Restful 介绍及SpringMVC+restful 实例讲解
restful不是一个框架,称为一种编码更烦更贴切吧,其核心类位于spring-web.jar中,即RestTemplate.class restful是rpc通过http协议的一种实现方式,和web ...
- RESTful API URI 设计的一些总结
非常赞的四篇文章: Resource Naming Best Practices for Designing a Pragmatic RESTful API 撰写合格的 REST API JSON 风 ...
- RESTFUL API 安全设计指南
RESTFUL API 安全设计指南 xxlegend · 2015/10/18 15:08 0x01 REST API 简介 REST的全称是REpresentational State Trans ...
- Restful.Data,现招募有为骚年,群号 338570336
光阴似箭,日月如梭,套用小学作文惯用的一句开场白来开始重新开始我的博客园生涯吧. 8年的风霜雪雨,不断的击打着我内心的哀伤,可我依旧坚挺的屹立在这里,是因为技术是我一直坚持的梦想. 追寻着先辈和高人的 ...
- 追踪app崩溃率、事件响应链、Run Loop、线程和进程、数据表的优化、动画库、Restful架构、SDWebImage的原理
1.如何追踪app崩溃率,如何解决线上闪退 当 iOS设备上的App应用闪退时,操作系统会生成一个crash日志,保存在设备上.crash日志上有很多有用的信息,比如每个正在执行线程的完整堆栈 跟踪信 ...
- 使用CodeIgniter框架搭建RESTful API服务
使用CodeIgniter框架搭建RESTful API服务 发表于 2014-07-12 | 分类于 翻译笔记 | 6条评论 在2011年8月的时候,我写了一篇博客<使用Cod ...
- FJNU 1159 Fat Brother’s new way(胖哥的新姿势)
FJNU 1159 Fat Brother’s new way(胖哥的新姿势) Time Limit: 1000MS Memory Limit: 257792K [Description] [题目 ...
- 虚拟研讨会:如何设计好的RESTful API?
http://www.infoq.com/cn/articles/how-to-design-a-good-restful-api/ REST架构风格最初由Roy T. Fielding(HTTP/1 ...
随机推荐
- moviepy音视频剪辑:使用fl_time进行时间特效处理报错OSError: Error in file xxxx, Accessing time
☞ ░ 前往老猿Python博文目录 ░ 老猿在使用moviepy音视频剪辑的fl_time进行时间特效处理时,系统报错: OSError: Error in file F:\video\WinBas ...
- 第15.41节、PyQt(Python+Qt)入门学习:输入部件QComboBox组合框功能详解
专栏:Python基础教程目录 专栏:使用PyQt开发图形界面Python应用 专栏:PyQt入门学习 老猿Python博文目录 一.概述 Designer中输入工具部件中的Combo Box组合框与 ...
- PyQt(Python+Qt)学习随笔:QTableWidgetItem项文本和项对齐的setText、setTextAlignment方法
老猿Python博文目录 专栏:使用PyQt开发图形界面Python应用 老猿Python博客地址 QTableWidget部件中的QTableWidgetItem项的文本可以通过text()和set ...
- PyQt(Python+Qt)学习随笔:QAbstractItemView的iconSize属性
老猿Python博文目录 老猿Python博客地址 视图的iconSize属性用于控制显示icon的项上的icon图标大小,在视图可见情况下设置该属性会导致视图上的显示项重新调整布局. 可以使用ico ...
- HTML5中的自定义属性data-*
在html5中,给元素添加自定义属性需要用到data-*,比如data-name,添加完data-自定义属性之后通过元素的dataset属性来访问其值. dataset与getAttribute/se ...
- 手把手教你写DI_0_DI是什么?
DI是什么? Dependency Injection 常常简称为:DI. 它是实现控制反转(Inversion of Control – IoC)的一个模式. fowler 大大大神 "几 ...
- CF1000F One Occurrence
本题解用于记录一下一个优秀的东西--懒标记. 题解 可以很轻易的想到莫队的做法,但是题目让你输出的是满足条件的一个数,而不是满足条件的数的个数,似乎很难去 \(O(1)\) 转移.这个时候我们的懒标记 ...
- JetBrains系列产品使用记录
1.PyCharm中from import提示找不到定义,提示错误,但其实是没有错误的 右键项目的根路径,Mark Directory As Source Root 2.自动换行 在Editor-& ...
- Python零散知识点记录
1.关于setdefaultencoding之前必须reload(sys): 要在调用setdefaultencoding时必须要先reload一次sys模块,因为这里的import语句其实并不是sy ...
- 【Azure Redis 缓存】Azure Redis 服务不支持指令CONFIG
问题描述 在Azure Redis的门户页面中,通过Redis Console连接到Redis后,想通过CONFIG命令来配置Redis,但是系统提示CONFIG命令不能用. 错误消息为:(error ...