RESTful API 设计约定

目的

本文编写目的是为了尽可能的约定好一个公司的产品、组件、项目开发的RESTful API 设计风格,使不同团队间设计的API风格尽量一致,减少项目后期由于规范问题或设计不足导致的接口重构造成的开发、测试返工。最终让接口的最终使用者能够在开发过程中有个良好的体验。

此约定可作为开发人员设计RESTful 接口或项目接口发布评审的参考。

个人观点:用了 JSON-RPC 不等于 是RESTful API,RESTful API通常是基于HTTP/JSON方式实现的 ,两种方式的API设计方式都不错,项目中选适合的就好。简单对比如下:

  • JSON-RPC: 通常采用POST Method,路径为类名方法名拼接,接口发布快捷,可以通过框架自动化,技术上无需人工参与。规范约束性小、易扩散不好管控,适合系统内部接口调用。实际情况是这种RPC模式调用中,为了提升性能,通常都采用类似Thrift 或 protobuf框架,相比基于文本的JSON序列化性能更高。
  • RESTful API : 需要人工规划和设计的API接口,更加容易理解、设计统一且美观。风格约定内容相比RPC更多,适合做用户接口Open API的规范 。通过网关对外发布接口时,强烈建议要采用这种设计风格。

本文仅是作者个人根据主观喜好和接口设计经验搜罗总结而来的RESTful API设计约定,仅作为接口设计的基本要求,也欢迎与大家讨论。此约定未涉及超文本HATEOAS相关内容,也不包含RPC类面向后端服务方法映射接口的范畴。

API 设计的关键点

API 是后端应用程序的脸面(UI),用户体验非常重要。尤其是当你开发的是一个可复用的组件或产品,如果API设计有些许瑕疵,会直接影响开发者的体验,设计者会被骂的…… 有问题的API一旦开放出去了,哪怕是简单的拼写错误,由于要保持兼容性不能删改,会成为技术欠债长期背下去。

  • 规范约定:在设计API时能遵守业界常用规范或约定,不要特立独行
  • 简单易用:好的API要简单、直观、容易理解且方便使用
  • 风格统一:API定义的风格要统一,要有规律可循。如命名方式、使用方式等等
  • 可用高效:首先API肯定保障功能的可用性,然后是尽量完善与灵活

以上关键点不只适用于RESTful API,其他类型API也是一样的。

URL Root 根路径定义

作为对外公开发布的RESTful API,根URL中一般要有域名与版本信息。通常一个WEB站点会同时提供网站服务和API服务,我们需要根据URL能够区分出访问的是服务接口还是网站中的网页、文件等资源。因此RESTful API的根URL中根据不同场景一般会在一级域名中或者是子域名中带api关键字。

常见的两种根URL用法如下:

  1. https//api.example.com/v1/* [example.com同时提供网站和API服务]
  2. https//xxx.example.org/api/v1/* [example.com拥有多个子域名,且子域名会同时提供网站和API服务]

推荐的方案是根URL中采用子域名的方式区分API,即: https://iam.demo.com/api/v1/*

  • 在企业微服务架构中,对外公开发布API的根URL定义应该通过API Gateway统一设置
  • 各子系统内部应用间开放的API不必考虑域名和版本问题,应用中一旦加了多版本就相当于一个源码项目内需要维护多套服务发布的代码,这种方式不推荐使用
  • 独立项目没有网关时,请根据项目需求设计

URL Endpoint 路径终点资源命名

路径终点即粗体部分内容: https:// example.org/api/v1/menus

  • 面向资源的方式定义路径终点,统一采用名词+复数形式如:/users/process-instances

  • 终点直接以资源路径开始即可,不必加应用名或版本前缀。版本由API Gateway控制,应用上下文由容器统一配置

  • URL Path 风格用蛇形样式(snake-case)还是用驼峰样式(camelCase)???

    • 不建议用驼峰命名法,因为一般URL大小写不敏感。(?查询参数可以使用驼峰命名法)
    • 推荐用蛇形命名法,多个单词用中线-分割,如/process-instances ,URL path不要使用下划线_,因为在部分超链接显示风格中会有下划线,与path中下划线重叠很难看清楚
  • 下划线与中线

HTTP Method 说明

设计RESTful API时常用的HTTP Method包括:GET、POST、PUT、PATCH、DELETE 简单说明如下:

  • GET:从服务器查询资源数据,可能返回一个或多个资源。
  • POST: 在服务器上新建一个资源,返回信息根据实际功能需要设计。
  • PUT: 更新服务器上的一个或多个资源,返回信息根据实际功能需要设计。
  • PATCH:更新一个资源的部分属性,返回信息根据实际功能需要设计。
  • DELETE: 删除服务器上的一个或多个资源,返回信息根据实际功能需要设计。

路径变量与查询参数如何选?

唯一定位单个资源用路径参数

根据资源标识可以唯一定位一个资源时,建议使用URL路径参数方式传递。对应Springboot 的 @PathVariable。API Path示例如下:

  • 根据用户账号查询一个用户信息 GET /users/{username}
  • 根据用户账号删除一个用户数据 DELETE /users/{username}

过滤一或多个资源用查询参数

根据资源属性查询过滤一或多个资源时,建议使用URL查询参数方式传递。对应Springboot的 @RequestParam。API Path示例如下:

  • 根据用户姓名过滤查询 GET /users?realNameLike=jack&gender=M
  • 根据账号状态删除已注销的用户数据 DELETE /users?status=3

复杂条件查询参数怎么传?

对于简单查询类接口,可以使用路径参数和查询参数解决,如果是复杂功能型查询接口中需要通过复杂的过滤条件查询时如:> < in between等等,查询参数用起来会非常痛苦,GET Method又不支持提交Request Body参数。因此我建议这种复杂型查询采用POST Method 提交到一个特定的Path上。参见如下场景:

  • 使用复杂条件查询学生信息POST /students/search Request Body 中提交复杂查询条件,示例如下(此处的body报文格式仅为示意而非标准):
{
  "table" : "student",
  "where" : {
    "condition" : {
      "and" : [ {
        "or" : [ {
          "field" : "name",
          "op" : "like",
          "values" : [ "子涵" ]
        }, {
          "field" : "name",
          "op" : "like",
          "values" : [ "梓涵" ]
        } ]
      }, {
        "field" : "age",
        "op" : "between",
        "values" : [ 18, 20 ]
      }, {
        "field" : "address",
        "op" : "like",
        "values" : [ "上海" ]
      } ]
    }
  },
  "select" : [ "name", "age", "mail", "address" ],
  "order" : [ {
    "field" : "age",
    "type" : "desc"
  } ]
}

分页与排序

查询接口返回多个数据时,需要支持分页(枚举类数据或少量数据除外)和排序。

如需使用分页查询和排序,建议统一请求与响应报文结构,格式如下:

请求参数示例:

GET /users?page=1&size=5&sort=username

单页数据响应结果示例:

{
    "content": [
        {
            "id": 6,
            "username": "allen"
        },
        {
            "id": 7,
            "username": "alice"
        },
        {
            "id": 8,
            "username": "bob"
        },
        {
            "id": 9,
            "username": "brown"
        },
        {
            "id": 10,
            "username": "bond"
        }
    ],
    "totalPages": 4,
    "last": false,
    "totalElements": 19,
    "number": 1,
    "size": 5,
    "sort": "username",
    "first": false,
    "numberOfElements": 5
}

上述分页排序与响应报文格式是来自Spring Data定义的模型,为了保持分页排序接口相关的使用习惯,如果持久化不使用JPA,仍然建议采用上述规范的报文定义封装接口。为使用者提供一致的体验。

资源操作类接设计

如果资源需要做一些增删改之外的操作(如状态变更),可以用/actions作为path

例如:流程平台中的流程实例会有状态变化,如启动、挂起、恢复、终止等等,这种接口建议这样设计:

  • PUT /process-instances/{proocessInstId}/actions/{action}, action可以是start,suspend,recovery,terminate等等。具体的状态变迁需要传递的业务参数通过Request Body 提交。

  • action对应的HTTP Method优先采用PUT。如果具体操作无法满足幂等(如:资源复制、移动),可以选择用POST。

资源层级关系接口设计

组合资源通过所属资源入口访问

组合资源,即两种资源之间存在组合关系,组合指整体与部分的强包含关系,但整体与部分是不可分的,整体的生命周期结束也就意味着部分的生命周期结束。对"部分"的操作一定会由整体作为入口,不会直接跳过"整体"来对"部分"做增删改查。这种组合场景中,推荐API设计方式示例如下:

  • 示例:获取流程实例的上下文变量查询接口

    • GET /process-instances/{processInstId}/variables/{varPath}
  • 示例:获取人员的个人概况相关信息

    • GET /users/{username}/profiles

聚合资源入口独立,避免路径嵌套访问

聚合资源,即两种资源之间存在聚合关系。聚合也是整体与部分的弱包含关系,但整体与部分之间是可分离的,他们可以具有各自的生命周期,部分可以属于多个不同的主体对象,也可以为多个整体对象共享。

例如,机构或角色下包含人员,需要获取机构或角色下的人员的场景,避免做成分别通过机构入口或角色入口找人等重复的具有类似功能的接口:

  • 不推荐:GET /organizations/{orgId}/usersGET /roles/{roleId}/users
  • 推荐:GET /users?orgId=xxx/users?roleId=xxx 这种根据条件过滤用户资源的接口形式

HTTP Status 说明

在RESTful API设计中,正常和异常情况建议通过HTTP约定的status进行区分,不建议采用所有接口均POST Method调用,永远返回200这种模式。

推荐的常用Http Status说明如下:

  • 成功 2xx,常用情况说明如下
    • 200 OK ,常用状态码,通常表示请求成功
    • 201 Created ,POST \ PUT \ PATCH,通常表示资源新建或修改成功
    • 202 Accepted,服务端已接收请求,通常在服务端异步处理场景中使用
  • 重定向 3xx
    • 重定向一般是后端的控制器在需要重定向时返回给浏览器的状态码,比如用户未登录或会话超时跳转登录页时的重定向登录页。在业务功能类RESTful API设计时通常不使用3xx相关的状态码,因为通常前端调用后端会采用Ajax方式发送请求,这种方式前端无法接收到3xx的响应,会被浏览器拦截但却不能自动重定向,浏览器会提示异常。
    • 通常业务功能RESTful API在用户认证不通过时,会返回401状态码,表示未认证,同时在Response中提供登录跳转地址,前端收到请求后,通过程序处理跳转登录页。
    • 如果是专用于登录跳转的API也可返回3xx状态码,但这类API不应该通过Ajax方式调用。
  • 客户端错误 4xx
    • 400 Bad Request,无法识别客户端请求
    • 401 Unauthorized,未通过服务端认证
    • 403 Forbidden,拒绝访问,通常是通过认证但权限不足
    • 404 Not Found,访问的资源不存在
  • 服务端错误 5xx
    • 500 Internal Server Error,服务端内部错误,这种请下,建议在Response Body中提供错误信息。

HTTP 1.0 Status 详细说明参考

正常响应报文

API调用成功后,返回HTTP 2xx状态码,Response Body直接返回业务数据即可。请求和响应报文建议统一采用JSON格式。

异常响应报文

RESTful API 对于异常报文需要规范和统一,服务端出现异常情况下,需要进行全局拦截,然后将异常信息封装为规范的格式,返回给调用端。

对于后端的异常信息,建议包含编码和消息

  • message:消息是使用者可理解的文本内容,可以根据客户端的Accept-Language返回对应的语言的错误提示信息。

  • code:后端错误对应的编码,可以根据编码编制说明手册,前端也可以根据编码进行二次的异常信息统一管理和优化后展示给用户。

  • 推荐以Spring boot为基础的统一异常响应报文如下示例:

    {
        "timestamp": "2018-05-25 15:02:23",
        "status": 403,
        "error": "Unauthorized",
        "exception": "com.ptp.infra.commons.exception.PTPRuntimeException",
        "message": "User unauthorized , username : test",
        "path": "/users/test",
        "code": "IAM.000001"
    }
    

结尾

本文是基于学习各路大神们对RESTful 设计相关文章,结合自己设计接口时遇到困惑后的解决方案,收集与总结而成的RESTful API设计约定。部分内容夹杂个人喜好与主观观点,抛砖引玉,希望能为大家设计API带来些许帮助。后如果遇到一些更复杂的场景,欢迎一起沟通。

参考与引用

转载本文需注明出处:RESTful API 设计约定

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容