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.primeton.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://iam.primeton.com/api/v2/process_instances]在本身带有下划线风格的超链接样式中识别困难
    • 推荐使用中线,[http://iam.primeton.com/api/v1/process-instances]
    • 更重要的是统一

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报文格式仅为示意而非标准):
{
    "criteria": {
        "table": "student",
        "where": {
            "and": {
                "expr": [{
                    "field": "name",
                    "op": "in",
                    "value": ["a", "b", "c"]
                }, {
                    "field": "age",
                    "op": "eq",
                    "value": "10"
                }],
                "or": {
                    "expr": [{
                        "field": "address",
                        "op": "like",
                        "value": "上海"
                    }],
                    "and": {
                        "expr": [{
                            "field": "age",
                            "op": "between",
                            "value": ["6", "8"]
                        }, {
                        "field": "mail",
                        "op": "eq",
                        "value": "[email protected]"
                    }]
                    }
                }
            }
        },
        "select": "name,age"
    }
}

分页与排序

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

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

请求参数示例:

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": 404,
        "error": "Not Found",
        "exception": "com.ptp.infra.commons.exception.PTPRuntimeException",
        "message": "User not found , username : test",
        "path": "/users/test",
        "code": "IAM.000001"
    }
    

结尾

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

参考与引用

  • HTTP 1.0 Status规范:RFC2616-SEC10
  • RESTful & JSON API
  • RESTful API 设计指南
  • 普元DevOps 产品API设计规范(内部链接)

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

你可能感兴趣的:(RESTful API 设计约定)