RESTful API设计指南

关于RESTful API

RESTful API是一种软件架构风格、设计风格,而不是标准,只是提供了一组设计原则和约束条件。它主要用于客户端和服务器交互类的软件。基于这个风格设计的软件可以更简洁,更有层次,更易于实现缓存等机制。

RESTful API当前已经很常见,但参考了很多网络上的文章,各方实现的标准和规范都不相同,有的追求完全遵循HTTP标准的,有的是基本遵循但相对不严格的,有的则是RESTful和RPC混用的。

如果要说RESTful API有没有一个绝对的标准,那只能是HTTP标准,但尴尬的是在一些项目的实际开发中,HTTP标准的复杂性甚至超过了项目本身。

因此,本文可以理解为一份轻量级的RESTful API规范,相对来说没有那么周全和严谨,用于一些小型项目的开发。

请求

API版本号

API应该包含版本信息,主要有两种方式:

一种是在URL中放置版本号

example.com/api/v1/*

另外一种是将版本号放在HTTP Header

Accept: application/vnd.example.com.v1+json

其中 vnd 表示 Standards Tree 标准树类型,有三个不同的树: xprsvnd。你使用的标准树需要取决于你开发的项目

  • 未注册的树(x)主要表示本地和私有环境

  • 私有树(prs)主要表示没有商业发布的项目

  • 供应商树(vnd)主要表示公开发布的项目

后面几个参数依次为应用名称(一般为应用域名)、版本号、期望的返回格式。

推荐采用第一种方式,即在URL中嵌入版本号的方式,这种做法版本号直观、易于调试。

另外,API必须保持向后兼容,引入新版本API的同时确保旧版本API仍然可用。

URL格式

表示资源集合的URL

/employees

表示具体某个资源的URL

/employees/1

查询字符串

/employees?firtName=tom

非资源请求

有时API调用并不涉及资源(如翻译、计算或搜索),API响应不会返回任何资源,而是执行一个操作并将结果返回给客户端,有时候强行使用名词表示反而不直观。这种情况下允许有例外,可以在URL中使用动词而不是名词,来清楚的区分资源请求和非资源请求,例如:

GET /translate?from=de_DE&to=en_US&text=Hallo
GET /calculate?para2=23&para2=432
GET /search?query=Paul

过滤

如果记录数量很多,服务器不可能都将它们返回给用户。API应该提供参数,过滤返回结果。 下面是一些常见的参数:

?limit=10:指定返回记录的数量
?offset=10:指定返回记录的开始位置。
?page=2&perPage=100:指定第几页,以及每页的记录数。
?sortby=name&order=asc:指定返回结果按照哪个属性排序,以及排序顺序。
?animalTypeId=1:指定筛选条件

HTTP方法

幂等的定义:简单说来就是一个操作符合幂等性,那么相同的数据和参数下,执行一次或多次产生的效果(副作用)是一样的。

2个URL乘以4个HTTP方法可以组成一组功能:

  POST(创建) GET(读取) PUT(更新) DELETE(删除)
/employees 创建一个新员工 列出所有员工(ID和名称,不要太详细) 批量更新员工信息 删除所有员工
/employees/56 (错误) 获取56号员工的信息 更新56号员工的信息 删除56号员工

HTTP Header

响应

无返回数据实体

{
    "code": 0,
    "message": "ok"
}

有返回数据实体

{
    "code": 0,
    "message": "ok",
    "data": null
}
{
    "code": 0,
    "message": "ok",
    "data": {
        "id": 2,
        "firstName": "harry",
        "lastName": "potter",
        "age": 22
    }
}

分页数据实体

{
    "code": 0,
    "message": "ok",
    "page": 1,
    "perPage": 3,
    "pageCount": 4,
    "total": 12,
    "data": [
        {
            "id": 1,
            "firstName": "Tom",
            "lastName": "Cruise",
            "age": 28
        },
        {
            "id": 2,
            "firstName": "Harry",
            "lastName": "Potter",
            "age": 22
        },
        {
            "id": 3,
            "firstName": "Mike",
            "lastName": "Johnson",
            "age": 20
        }
    ]
}

错误处理

错误响应格式:

{
    "code": -1,
    "message": "error"
}

HTTP状态码

如果使用SpringMVC,则默认已定义的HTTP状态码有:

状态码 用途
200 GET、POST、PUT、DELETE请求成功后均返回200状态码
400 请求有误时返回,一般是由于请求数据和格式不符合服务器要求
404 请求了一个不存在的资源,一般为URL有误时返回
406 请求的实体类型不对,例如客户端Accept要求返回”application/xml”,服务器端只能返回”application/json”,那么就会返回406状态码
500 请求发生错误,一般是服务器执行请求异常时返回

简单起见,暂不修改或扩展SpringMVC默认定义的HTTP状态码及使用场景

另附:常用的HTTP状态码

2开头,成功状态码。

状态码 原因短语 含义
200 OK 服务器已成功处理请求
201 Created(已创建) 对那些要服务器创建对象的请求来说,资源已创建完毕
202 Accepted(已接受) 请求已接受,但服务器尚未处理
204 No Content(没有内容) 响应报文包含一些首部和一个状态行,但不包含实体的主体内容

3开头,重定向状态码。

状态码 原因短语 含义
301 Moved Permanently(永久移除) 请求的URL已移走。响应中应该包含一个Location URL,说明资源现在所处的位置
302 Found(已找到) 与状态码301类似,但这里的移除是临时的。客户端应该用Location首部给出的URL对资源进行临时定位

4开头,客户端错误状态码。

状态码 原因短语 含义
400 Bad request(坏请求) 告诉客户端它发送了一条异常请求
401 Unauthorized(未授权) 与适当的首部一起返回,在客户端获得资源访问权之前,请它进行身份认证
403 Forbidden(禁止) 服务器拒绝了请求
404 Not Found(未找到) 服务器无法找到所请求的URL
406 Not Acceptable(无法接受) 客户端可以指定一些参数来说明希望接受哪些类型的实体。服务器没有资源与客户端可接受的URL相匹配时可使用此代码
410 Gone(消失了) 除了服务器曾持有这些资源之外,与状态码404类似

5开头,服务器错误状态码。

状态码 原因短语 含义
500 Internal Server Error(内部服务器错误) 服务器遇到了一个错误,使其无法为请求提供服务
503 Service Unavailable(未提供此服务) 服务器目前无法为请求提供服务,但过一段时间就可以恢复服务

HTTP Header

认证

/employees?firtName=tom?accessToken=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsImlhdCI6MTUzNjA0ODQ0NX0.1JEEnjcbQ3kzJ5YZ1Px1ljBaYcpvooRN9Gn9Sf7wB8o

有安全要求应该使用HTTPS,并且accessToken通过HTTP Header传递

如需要经常对接第三方系统,则应当采用OAuth2.0

示例

列出所有员工信息

请求:

GET /api/v1/employees HTTP/1.1
Host: localhost:8901
Accept: application/json

响应:

HTTP/1.1 200
Content-Type: application/json:charset=UTF-8
Transfer-Encoding: chunked
Date: Wed, 29 Aug 2018 02:06:19 GMT

{
    "code": 0,
    "msg": "ok",
    "data": [
        {
            "id": 1,
            "firstName": "Tom",
            "lastName": "Cruise",
            "age": 28
        },
        {
            "id": 2,
            "firstName": "Harry",
            "lastName": "Potter",
            "age": 22
        }
    ]
}

列出所有员工信息(分页)

请求:

GET /api/v2/employees?page=1&perPage=5 HTTP/1.1
Host: localhost:8901
Accept: application/json

响应:

HTTP/1.1 200
Content-Type: application/json:charset=UTF-8
Transfer-Encoding: chunked
Date: Wed, 29 Aug 2018 02:06:19 GMT

{
    "code": 0,
    "msg": "ok",
    "page": 1,
    "perPage": 5,
    "pageCount": 3,
    "total": 12,
    "data": [
        {
            "id": 1,
            "firstName": "Tom",
            "lastName": "Cruise",
            "age": 28
        },
        {
            "id": 2,
            "firstName": "Harry",
            "lastName": "Potter",
            "age": 22
        },
        {
            "id": 3,
            "firstName": "Mike",
            "lastName": "Johnson",
            "age": 20
        },
        {
            "id": 4,
            "firstName": "Lebron",
            "lastName": "James",
            "age": 36
        },
        {
            "id": 5,
            "firstName": "Rajon",
            "lastName": "Rondo",
            "age": 31
        }
    ]
}

列出所有员工信息,但要求返回的实体类型不受支持

请求:

GET /api/v1/employees HTTP/1.1
Host: localhost:8901
Accept: application/xml

响应:

HTTP/1.1 406
Content-Length: 0
Date: Wed, 29 Aug 2018 02:06:19 GMT

根据员工ID查询员工信息(URL传参)

请求:

GET /api/v1/employees/2 HTTP/1.1
Host: localhost:8901
Accept: application/json

响应:

HTTP/1.1 200
Content-Type: application/json:charset=UTF-8
Transfer-Encoding: chunked
Date: Wed, 29 Aug 2018 02:06:19 GMT

{
    "code": 0,
    "msg": "ok",
    "data": {
        "id": 2,
        "firstName": "Harry",
        "lastName": "Potter",
        "age": 22
    }
}

根据员工名字查询员工信息(查询字符串传参)

请求:

GET /api/v1/employees/?firstName=tom HTTP/1.1
Host: localhost:8901
Accept: application/json

响应:

HTTP/1.1 200
Content-Type: application/json:charset=UTF-8
Transfer-Encoding: chunked
Date: Wed, 29 Aug 2018 02:06:19 GMT

{
    "code": 0,
    "msg": "ok",
    "data": [
        {
            "id": 1,
            "firstName": "Tom",
            "lastName": "Cruise",
            "age": 28
        }
    ]
}

创建新的员工信息

请求:

POST /api/v1/employees HTTP/1.1
Host: localhost:8901
Content-Type: application/json
Accept: application/json

{
  "id": 3,
  "firstName": "Mike",
  "lastName": "Johnson",
  "age": 20
}

响应:

HTTP/1.1 200
Content-Type: application/json:charset=UTF-8
Transfer-Encoding: chunked
Date: Wed, 29 Aug 2018 02:06:19 GMT

{
    "code": 0,
    "msg": "ok",
    "data": {
        “id”: 3
    }
}

替换更新指定的员工信息

请求:

PUT /api/v1/employees/3 HTTP/1.1
Host: localhost:8901
Content-Type: application/json
Accept: application/json

{
  "id": 3,
  "firstName": "Mike",
  "lastName": "Johnson",
  "age": 20
}

响应:

HTTP/1.1 200
Content-Type: application/json:charset=UTF-8
Transfer-Encoding: chunked
Date: Wed, 29 Aug 2018 02:06:19 GMT

{
    "code": 0,
    "msg": "ok"
}

根据员工ID删除员工信息

请求:

DELETE /api/v1/employees/?firstName=tom HTTP/1.1
Host: localhost:8901
Accept: application/json

响应:

HTTP/1.1 200
Content-Type: application/json:charset=UTF-8
Transfer-Encoding: chunked
Date: Wed, 29 Aug 2018 02:06:19 GMT

{
    "code": 0,
    "msg": "ok"
}

参考文章

RESTful API设计规范

RESTful API最佳实践

RESTful API设计最佳实践

RESTful API编写指南

撰写合格的REST API

HTTP状态码详解

Table of Contents