KalaSearch:
这是一篇会长期更新的文章
什么样的 API 设计能被称为优秀当然是一个非常主观的标准,但是还是有一些客观可考量 API 质量的数据,比如
- 接了你设计的 API 的前端给好评的比例是多少,还是边接边骂
- 如果你的 API 本身就是你的产品的话(比如 Stripe,Algolia 或者 Github 等等),你的用户会对你的 API 好评吗
- API 是不是一读即可以清晰地知道,对应接口是做什么的。换句话说,接入 API 时需要的交流时间成本有多高
不管是前端程序员还是后端程序员,都少不了跟 API 打交道。后端需要把 API 设计和实现出来,而前端程序员需要把界面逻辑和 API 接起来,因此对于 REST 的设计规则有一些基本了解,不管你是前端还是后端,都会有很大帮助。
之前在厂里设计了一些还算被广泛使用的 API, 因此我写了这篇文章,结合之前的经验总结了一些要点。希望作为一个参考,可以帮助大家
文章请戳 => 优秀的 REST API 设计指南
当然我想要说明的是,设计 API 在一定范围内是有规律可循的,但是太过抠细节则会陷入无穷无尽地“宗教版”争论中,所以请大家理论讨论。
你们设计 API 的时候有些什么原则?有哪些好的规范和经验可以介绍和分享给大家?欢迎告诉我,我会加到文章中
KalaSearch:其它值得参考的文章:
https://stackoverflow.blog/2020/03/02/best-practices-for-rest-api-design/
https://hackernoon.com/restful-api-designing-guidelines-the-best-practices-60e1d954e7c9
https://www.vinaysahni.com/best-practices-for-a-pragmatic-restful-api
abbycin:我八股文写得特别好
baiyi:我认为在设计过程中,需要考虑 HTTP 方法的幂等性。比如 Github 的 Star 操作,为什么是 PUT 而不是 POST,就是从幂等性方面考虑的
KallyDev:补充一个微软在 GitHub 公开的规范,非常详细
https://github.com/microsoft/api-guidelines/blob/vNext/Guidelines.md
shijianit:如果要接口全部加密,get 方式请求,不是会暴露出来 id 数据吗?
xuanbg:修改密码和重置密码怎么设计?软删和硬删同时存在怎么办?
xuanbg:@shijianit 所以 id 不要用自增
bsg1992:这种只适合对外的 api 并且功能单一
一个 ERP 查询几十个字段你用 get?
gnozix:想问问下载接口应该怎么设计
gowk:@bsg1992 人家说的是在卡拉搜索的业务背景下,设计优秀 API 的最佳实践,你非要拿 ERP 来杠,有意思么
nockyQ:关于版本划分这一块,除了常见的 URI 版本控制之外还有其他两种。
https://restfulapi.net/versioning/
感觉楼主可以在这个基础上展开聊一聊。
lolizeppelin:paypal api 和沙箱比微信支付漂亮太多了
但是不妨碍 paypal 垃圾微信支付好用......
KalaSearch:@nockyQ 啊是的,stripe 用的是这种。感谢你的新信息
KalaSearch:@baiyi 感谢 <3
KalaSearch:@KallyDev 这个很赞,我之前也看过,谢谢提出来,我会加到文章里
KalaSearch:@shijianit id 应该默认认为已经暴露,藏不住。楼下说的用 uuid 是个好办法,不过不管怎么样不应该认为 id 可以隐藏起来达到安全的目的。(安全我懂得不多,更详细等楼下们讨论啦)
KalaSearch:@gnozix 能说说具体场景吗?文件下载?
wellsc:restfool (逃
MrTreasure:还是缺乏具体场景,文中的内容就是是属于 restful 的标准。但是对于难点没有很好的讲解,比如 restful 如何返回错误。区分 HTTP 错误以及业务错误
ZacksT:你的 REST API 满足公司 /研发团队标准就是好的设计。接口标准可以帮助开发者规避(公司研发 /团队研发)遇见过的问题或可能遇到的问题,也可以让组内代码标准化,统一化。
就拿一个简单的例子,一个分页查询的接口。其包含分页条件与不定量的查询条件。
你可以通过 GET 方式请求,将参数定义在 url 上。也可以通过 POST 方式请求,将查询参数定义在 requestbody 里。
第一个方式,在遇到查询条件复杂的情况下,会导致 url 过长。
第二种方式,又会产生很多 VO 的定义。
采用混搭又让前后端代码变得混乱。
我个人认为,只要满足团队要求的 API,就是好的 API 。具体实现各有好处,看团队取舍了
KalaSearch:@ZacksT 感谢回复。是的,满足团队、客户需求就是好 API 。对于你说的参数定义的例子,GET + URL 参数挺好的,遵从 REST 语义
@wellsc 不要淘气
bsg1992:@gowk 我并没有杠,你说回复也印证了我上面说的 [适合对外的 api 并且功能单一] r
ibreaker:小伙子很活跃啊,天天都能刷到你
lovedebug:@ZacksT 分页 GET 查询一般只带 limit 和 page,少量支持投影和过滤,如果有带其他复杂的 query 条件,其实更应该走 POST search 自定义方法
ieiayaobb:Get by Id 的比较明确,如果是 Get by name 这种,name 是唯一的怎么设计比较好?不想用 query 是因为不想在 name 不存在的时候返回空数组,而是希望也能和 Get by id 一样返回 404
xjchenhao:修改密码和重置密码,逼死强迫症?
xjchenhao:@xuanbg 修改密码和重置密码,逼死强迫症?
grzhan:之前负责撰写公司的 API 规范,当时也参考了很多包括 Azure ( https://docs.microsoft.com/zh-cn/azure/architecture/best-practices/api-design )、Google Cloud ( https://cloud.google.com/apis/design/ )等公司的规范,大厂的标准往往更加规范,给人很多对于 API 设计上概念理解的启发。
其中感觉最详细的大概是 Zalendo 的: https://opensource.zalando.com/restful-api-guidelines/ ,其中有非常多的实践是可以参考的,也像 RFC 一样规范了 MUST 、SHOULD 、MAY 的遵守分级。
关于文中提到的 REST 表示一个动作,我们参考的更多是 ElasticSearch API 的做法,即将动词加上下划线前缀,作为 POST 方法进行服务,形如: http://cloud.sy/machine/xxxx/_restart
关于这一块 Google API 是用冒号作为前缀的,但一些路由框架会占用冒号作为关键字,因此考虑使用下划线代替。
gnozix:@KalaSearch 对前台展示的表格数据,以 excel 的格式进行下载;所以需要下载的比较多。感觉 REST 风格不太容易表示需要下载的资源
wshcdr:值得看一下
Heanes:同意 8 楼,系统内部交互可能还是“常规”的设计形式
lovedebug:@grzhan 同负责撰写 API 规范,其实关于 list 操作的 filter 功能,在实际 API 设计中有些疑惑使用场景,因为大部分情况下使用一般的 query parameter 就可以解决。我的理解是一般的 query parameter 默认是 and 操作,缺乏 or 操作以及 range value 等功能,而 $filter 主要在 url 中描述若干参数复杂的逻辑运算,如果这么做用 POST 自定义动作不是更好吗?想听一下你的理解。
xuanbg:@lovedebug 要是支持复杂的 or 和 and 条件组合,url 参数就丑的要死了……不信的可以看 kibana
szthanatos:批量操作的实践为什么很少有人谈←_←
solee:经过几年的实践,我们最后全部统一了用 POST,之前看过一篇亚马逊写的关于 Restful API 设计的改进,加入动词的描述,感觉更合理
lovedebug:@szthanatos 微软规范有谈的
lovedebug:@xuanbg 哈哈 跟业务场景有关,如果不想用万能 POST,可能只能在 url query 中支持一些 or 查询,客户在使用我们的 public api 时提出的
lovedebug:@solee 自定义动词应该在已有的 RESTful 规范不满足时候才使用
Nolink:收藏了,谢谢分享
ericls:用现成的 query language 不好吗? 非要把 http headers 滥用成 query 还要自己定义 实现 维护……
Amit:@xuanbg
密码一般都是要做 hash 的,且不能暴露给前端,所以需要对这个字段单独修改,而不能放到完整信息中修改并返回,修改密码是在登录状态下,所以我会设计为 PUT /v1/users/{id}/password (管理员修改用户密码)或 PUT /v1/users/self/password (修改自己的密码),重置密码我理解为非登录状态下修改密码(不确定用户身份),所以我会设计为 PUT /v1/users/password,然后再 body 中提供用户名、验证码等信息。
软删除也是删除,对应用来说如果删除了就是不存在的,应用中不应该能看到,软删除和物理删除同时存在是不合理的,这种情况应该设计一个状态字段区分,而不是使用逻辑删除。
xuanbg:@Amit
修改密码和重置密码我也是一样的处理。在复数形式的资源后面,有时候不但要加动词,还得加属性,以定位到更细一层的资源才行。
我说的软删其实是禁用,只是为了理解方便。业务前端看不到了,也就没得用了。但元数据管理后端应该能看到,毕竟禁用后说不得还会启用。。。硬删当然就是数据灰灰,再也无法恢复的。如果软删用 PUT:/v1/users,那就和修改姓名冲突了。我是这样规划的,修改普通属性 PUT:/v1/users,禁用 PUT:/v1/users/status,删除 DELETE:PUT:/v1/users 。
jorneyr:RESTful 在 URL 里是禁止使用动词的,但是很多时候有的 URL 中用动词来表达很自然,强制使用 RESTful 的风格的话会很难受
imhxc:我一直有个问题,请教下。
在实际业务中,各种需求都有,很难严格遵守 RESTful API,拿文章中的示例来说:
GET /owners/1/pets/ 获取 id 为 1 的主人的所有宠物
1. 如果区分角色怎么办,比如管理员获取 id 为 1 的主人的所有宠物,结果中包含所有状态的宠物;
2. 其他人需要查看 id 为 1 的主人所有宠物,结果中只返回状态为「可公开」的宠物;
这种怎么设计?
codingbody:我有个问题问大家,为什么安全扫描的时候,不准我使用除了 GET 、POST 之外的请求,我认为请求的方式和安全没啥关系吧
DeWhite:那个就一句话,吃屎啦。就是没有主语的,国内的很明显主语省略的句子还有很多。
xcstream:这标题隐含意思就是不 rest 就不优秀(狗头)
KalaSearch:@imhxc 用 ACL 来控制,REST endpoint 没办法控制的
KalaSearch:@DeWhite 你说的是祈使句,祈使句当然可以没有主语(省略了第二人称主语)
forgaoqiang:看了下 Discuz Q,真的几斤,完全的 RESTFUL 风格,patch delete 各种方法都用
grzhan:@lovedebug 我个人觉得关于复杂查询不管是用 $filter 还是直接 POST 自定义方法(如 "_search" )都是可以的,具体看自己场景。
事实上我们项目实际实践中,这种情况还是自定义 POST 方法用的比较多
GavinZZ:??
GavinZZ:还有个叫车满满的。。。工资给开的还算可以 13K+ 14 薪,但是不推荐去,企业文化很奇葩
lovedebug:@grzhan 嗯。$filter 需要写 parser 专门处理,否则会重复造轮子
grzhan:@lovedebug 如果查询场景需求确实很复杂的业务的话,我们会考虑上 GraphQL 的
lovedebug:@grzhan 主要是 GraphQL 对已有产品的 RESTful API 破坏性过大,ROI 也不够高,另外也考虑在微服务和 k8s 中 GraphQL 中心化并不是一个很完美的方案。其实主要的阻力是项目进度和同事。哈哈哈哈
dongxiaoxian:好复杂
ChanKc:@codingbody 没有,但是历史上发生过一些 HTTP server 对 PUT,DELETE 等请求实现不当,导致远程代码执行等漏洞。一些公司就会觉得索性禁了这些请求更好
yixinlove:@KallyDev 好东西
wangxiaoaer:这个帖子很有启发啊,顺便问一下,针对楼上一些老哥们提到的复杂的组合条件查询,如果是基于 spring boot + jpa 的应用,如何优雅的实现呢?
cbasil:设计 API 的目的是为了前端好评? api 接口安全和效率都不需要考虑了吗?你去看看阿里,腾讯等大公司的接口文档,有几个是完全按照 REST API 来设计的。
lovedebug:@cbasil 一是对内为了公司内部统一,减少沟通成本。而是针对 public api 与主流统一,减少用户的集成成本。
nig001:不错的
fy:@lovedebug #32
这个我做了,默认 and 操作,请求类似这样:
/api/topic/list/1?time.ge=1577808000&order=time.desc&select=id,title
前端反馈一般,说是不好理解。语言是 python
https://github.com/fy0/slim
问题主要是几处:
1. http header 有限,有的查询条件放不下,其实同时支持提交 body 查询更好些( get 提交 body 是规范允许的,只是很多 http server 选择不解析)
2. 对查询的掌控力度不够。前端提交上来一个请求,说某种情况下希望将某个条件变成 or 查询,这时候做不到。当然这和 orm 还有底层实现有关,这是一个整体设计上的问题。
3. 连表查询比较复杂。
4. 全栈开发会觉得好用,有的纯前端就觉得这是后端偷懒。
所以可能不光是规范问题,还是框架问题,甚至要连同 orm 、表单验证、权限之类做通盘考虑。
@imhxc #44
角色权限 + ACL
sunzhenyucn:请让我默默地 mark 一下
lovedebug:@fy 感谢回复,是的,get 带参数会有这些问题。
一般对于 simple collection items 的 list(GET 方法)操作,我建议用 order,filter, 这样语义清晰,主要实现集合过滤功能。可以尝试在 filter= X OR Y 这样的形式实现 or 操作
我的理解是对于复杂集合(如 logs 等)或通用操作的模糊搜索还是用 POST + custom method,例如 /v1/items/search,除非可以细化复杂集合为若干简单的集合。
主要这个度不好把握。
当然,从实现简单程度来看,所有的 order,filter,projection 都可以定义为用 post 实现。
thtznet:看到 API 和表对应,我就知道不用看下去了,太水了。
jy28520:@KalaSearch 想问下我们现在的业务需要验证用户提交的 SKU 和优惠券是否匹配 请问 URL 应该怎么设计那?
我们会有几条 SKU 和几条优惠券的信息
b0644170fc:根本不需要 rest, get / post 走天下
imhxc:@fy 嗯嗯,ACL 是可以解决刚才提的问题。
但是总感觉 REST API 规范有局限性,自己曾经做过 ERP,会经常出现较为复杂的接口,感觉很难严格遵守 REST API 风格。
比如有一些无法区分上下级关系、获取同一个数据,有的需要用 iD 查,有的需要用 MD5 查,总之,实际业务中各种千奇百怪的需求。
我以前自己写接口用 REST API 写着写着就要精神分裂了。。。?
也可能是我没理解 REST API 的精髓?
no1xsyzy:@imhxc #70 除非你能直接塞图灵完备的代码进数据库,不然什么都有局限性
就是 SQL 有时不得不分成两个查询( SELECT ),虽然完全就是数据库里的内容,之后可优化为一次数据库交互包含两个查询(避免传输),但一个(对人脑来说)本来看上去非常简单的东西,不通过逻辑检验竟然无法简化。
实际上 RESTful 不是有局限性,而是它就是局限性本身:通过强加某种限制,将(一次) API 请求类比为对(一项)资源的操作,形成某种直觉映射,来理清思路。要 “改” 到 RESTful,并不是改动 API 就行的,而是整个建模得修改。
有人[谁?](忘了谁)认为其实是启发自 Unix 的文件操作。(所以 WebDAV 是 RESTful 最恰当的应用场景)
imhxc:@no1xsyzy 感谢,涨知识了。
lolizeppelin:这个论坛早就有人说过了
RESTful 是对 sql 的劣质模仿,没法表达的情况多去了
no1xsyzy:@lolizeppelin #73 谁?在哪儿说的?
RESTful sql 劣质模仿 site:v2ex.com 只搜出来你说的话……
从来从来,RESTful 就是个和 SQL 完全相悖的路线
SQL 一直在做得越来越图灵完备,添加各种诡异的、我承认确实像是有那么回事儿的、但其实没有也没关系的功能进去。
RESTful 一直都是那么平铺直叙。谓宾仍然是谓宾,最多用点 HTTP 语义。
“C 是个对 Lisp 的劣质模仿”
lolizeppelin:@no1xsyzy
est 说的 嘿嘿
no1xsyzy:@lolizeppelin #75 @est 在哪说的?
楞是没搜到……
lolizeppelin:@no1xsyzy
当然个别字有出入呗,你找他 233333
est:@lolizeppelin
@no1xsyzy
我也不记得在哪里说的了,但是中心思想是,RESTful 本来是对文件读写的一个 增删改查 的封装,最适合拿来做 WebDAV 之类的工具。然而其他的业务的「动作」很可能无法用这 4 个指令覆盖。就多出来了很多奇葩的指令比如 OPTIONS TRACE PATCH 。。。与其这样,还不如直接根据具体业务在 url 里指定动作名称。比如
POST /api/user/login
POST /api/order/cancel
然后我是明确反对把 URL 里直接嵌入 resource id 作为路径一部分的。比如 GET /myitem/12345/ 这种,RESTful 一时爽,nginx 日志分析火葬场。
no1xsyzy:@est #78 本来指令就随便添加,过分绑定到固定四个指令有点先辈的罪或者思维定势。
我觉得 POST .../login 没什么问题,我的某个工具里面 Login 是类名,将 Login 视为名词形式。
同时我觉得 POST .../order/cancellation 也没什么问题,是订单状态改变。DELETE order 和它是根本上不同的两种行为。如同 rm 一样,DELETE 谓词的使用应当慎之又慎。
一般这类框架会有自己的日志的,不用 nginx 分析日志。而且如果不分 /api/* 的 URL 出来的话,也就是 /static/* 让 nginx 处理,其他都归框架管了。而且看到某 PHP 应用的官方部署教程是关掉 /static/* 的日志的…… 基本上 nginx 日志存在有意义的信息就已经是系统层面的大问题了(比如 uwsgi 挂了)
putaozhenhaochi:老哥这么拼
iplayio2019:@est /user/login 这种可以抽象成 session 资源,restful 很强调“资源”概念,POST /api/sessions,登录就是创建 session 。
注销登录 DELETE /api/sessions/me
取消订单本身就是状态更新,PATCH /api/orders/{orderID}
<status>:<取消状态的值>
est:@iplayio2019
那么问题来了
1. 一次登入多个站点的 SSO 怎么设计 URL
2. 订单拆分、合并操作如何表达?
3. 上面的同学提到的,批量操作如何写{orderID} ?
lovedebug:@est
RESTful 规范描述的是资源,对于非资源的情形一般需要自定义 action,这一方面大厂已经做了详细的设计,落实到具体设计就根据各自情况做了
比如你的描述提到的
1, 一般写成 POST /users/${userId}/login?type=sso 或者 login?user=xxx & type=xxx
2,一般会写成 POST /orders/${orderId}/$spilit 或者 POST /orders/${orderId}/$merge {ids:[]}
3 一般写成 POST /items/$batchUpdate {ids:[]}
est:@lovedebug 其实你 2 和 3 已经是另外一种风格的 URL 设计了。。。还不如干脆一条路走到黑全按照这种风格来设计
1. POST /user/login
2. POST /order/split POST /order/merge
3. POST /item/batchUpdate
多干净统一。
RESTful 就是被 UNIX 那种「所有东西都是文件」思想毒害的。遇到完全不像文件或者资源的东西,瞎搞。
lovedebug:@est 对于自定义 action,RESTful 本来就没有统一,自定义 API 风格各个团队根据自己需要定义就可以
两种方案
1. 将资源 uuid 描述在 URL 中
2. 将资源 uuid 描述在 body 中
我们两人上面的就是这两种方案的体验,没有好和坏,只看对于 API 使用者的可读性。
微软和谷歌,github 对于自定义 action 也是分别有自己的实现
no1xsyzy:@est #84 问题不在于 “一切皆文件”,而在于纯远端操作。
文件是对于 “可读可写” 的抽象。举上述你提到的例子:
1. 登录是一个状态,SSO 是一个多服务端共享的状态,可读可写,而且读写经过客户端传递,与文件这一抽象完美契合不成问题。
sub_site 302 到 //sso_host/user/login?to=sub_site/user/login
然后由 sso_host 确定后 302 传递 token //sub_site/user/login?with_token=~~token~~
类比: $ cat sso/token | authorize sub_site
2. 拆分合并的核心在于它是个纯远端操作。一般来说在 Unix 下拆分文件,不出意外是 head|tail 或者 awk/sed 之类,或者对特定的文件类型也是专门的提取器而不是单独的拆分装置。然而无法保证拆分的准确性。那么显然,正确的操作应当是写一个专门的脚本完成这件事 —— 类比过来,就是新谓词。
SPLITORDER /orders/<orderId>,请求体发送拆分准则之类的,返回 200 内容是拆分结果。
类比: $ split_order "site/orders/${orderId}" [--options ...]
site/orders/a site/orders/b site/orders/c
3. 批量操作可以借用 glob,也可以是单独谓词。后者不必说,前者比如:
PATCH /orders/{a,b,c}
Content-Type: application/json
{"coupon": "foobar"}
est:> SPLITORDER /orders/<orderId>,请求体发送拆分准则之类的,返回 200 内容是拆分结果。
对对对。。就是喜欢 RESTful 原教旨主义者这种一本正经发明 1000 个新词的想法。。。。
反正我对 http 的 verb 就认同 2 个,读是 GET, 写就是 POST 。你们觉得 1000 个新词最血统纯正我也没办法。。。。
est:@lovedebug 我也差不多是这个观点。RESTful 其实没必要完全照搬。取其精华,去其糟粕。团队内部统一认识就行。
ChristopherWu:@est
@no1xsyzy
@lovedebug
RESTFul 是不是还有一个问题就是,强绑定于 HTTP ?
假如我的业务除了 HTTP 接口给外部团队使用外,还要提供二进制协议的接口给内部团队使用,那么 Restful 就不通用了。
相对来说,使用 RPC 风格的协议,可以统一 API,不管什么,code,message,data 三个字段封装起来就是业务层,其他 code 是协议层的事情。
brickxu:你这涉猎够广的,另外一个贴子里还在分析 NoSQL,这边直接换到 API 设计了,然后全是 kala 搜索的域名。
no1xsyzy:@est #87 奇妙,我根本不是原教旨主义者
我跟你讲,原教旨主义者的看法是,拆分订单就是删除旧订单,然后建两个新的。
至于批处理,原教旨主义者认为就应该发 100 个请求,服务器被撑爆就应该扩容、增技术 balabala 。
建新谓词是传播主义者的一支,把 RESTful 当 RPC 用。
est:> 我跟你讲,原教旨主义者的看法是,拆分订单就是删除旧订单,然后建两个新的。
> 至于批处理,原教旨主义者认为就应该发 100 个请求,服务器被撑爆就应该扩容、增技术 balabala 。
> 建新谓词是传播主义者的一支,把 RESTful 当 RPC 用。
我石化了。。。。
Celery REST API - python有没有办法将Celery用于以下用途:使用Form参数将对外部URL的HTTP调用排队(HTTP Post to网址)外部URL将响应HTTP响应,200、404、400等,如果响应采用错误非200 ish响应的形式,将重试重试一定次数,并将根据需要退出使用REST API将Task / Job / Work队列添加到Celery,将URL传递给call…
api-mom 一个 API 在线管理工具andychen1:免费,安全,简单易用界面简洁。支持团队多人协作,只需安装 chrome 浏览器 api-mom 扩展就可以对 API 进行测试。 项目里的文件夹,接口列表,接口 Tab 都支持鼠标拖动排序。 编辑接口时支持常用快捷键 Ctrl + S 保存。 接口文档和测试结果并排,一切都在眼前,方便校对。 接口越来越多,可以用模糊搜索快速找出。 浏览模…
REST API Base64映像imagecreatefromstring():数据不是可识别的格式 - php我正在为Android应用开发REST API。我想将base64图像另存为从应用程序发送的jpeg图像。我在laravel干预下为此编写了代码,并与邮递员进行了测试。没问题。但是,当数据从Android App发送时,他们说我在收到错误Image intervention - Image source not readable时出现了500条错误。之后,我…
各种有知道有哪些稳定的 API 数据云服务厂商吗?inktiger:想在本地使用一个简单 html 做一个小东西,不想搭载服务,想到用 api 数据云,在云端设计数据库表,使用 api 用 ajax 调用进行增删改查就行,目前我知道的有 apicloud 支持这么玩,可我感觉他不靠谱,还有什么其他比较做的好的吗
请问 根据 REST api 生成 spring 客户端代码有什么办法?chenhui7373:输入: https://{api-host}/v1/player/immediateControl/power 请求参数 { "playerIds":[ "4PBXun3mQoZGnKdLKoDtBA==", "4PBXun32QoZGnKdLKoDtBA==" ], "option":1 } 响应参数 { "success":[ "…