restful API设计

RESTful API 已经非常成熟,也得到了大家的认可。本文主要讲的是在工作中遇到的一个比较被认同的“规范”,总结下自己的经验。

按照 Richardson Maturity Mode 对 REST 评价的模型,规范基于level2来设计。

考虑的问题

  1. 认证
  2. 安全
  3. 缓存:ETag,IF-NO-MATCH
  4. 同步或者异步
  5. 一致性
  6. 扩展

URL

/version/{catalogy}/{entities}/{entity_id}/action

其中

  • version : API 版本, 如 v1, v2, v3, v1beta, latest, v2alpha 等
  • category : 某种分类, 如 configuration, manage 等高层资源
  • entities : 实体资源, 如 images, services, networks 等, 为复数
  • entity_id : 实体资源的 id
  • action : 表示对资源的某种行为, 具体的行为在 body 中指定.
  1. 以复数(名词)进行命名资源,不管返回单个或者多个资源。
  2. URL 中单词统一小写, 如果由多个单词组成以 “-” 分割

操作

用HTTP动词(方法)表示对资源的具体操作。常用的HTTP动词有:

  • GET:从服务器取出资源(一项或多项)。
  • POST:在服务器新建一个资源。
  • PUT:在服务器更新资源(客户端提供改变后的完整资源,全部更新)。
  • PATCH:在服务器更新资源(客户端提供改变的属性,部分更新)。
  • DELETE:从服务器删除资源。
  • HEAD:获取资源的元数据。
  • OPTIONS:获取信息,关于资源的哪些属性是客户端可以改变的。

例子

/v1/images/{image_id}/members/{member_id}
/v1/images/{image_id}/members

/v3/{project_id}/groups/{group_id}/action : 对资源进行状态修改

{
    "reset_status": {
        "status": "available"
    }
}

URL 查询参数

请求参数在 ? 之后以 key=value&key=value 形式存在

排序

  • sort_key=[key1,key2] : 根据某个属性进行排序, 排序根据自然属性排序
  • sort_dir=[inc|desc] : 排序方向, 顺序(inc)或倒序(desc), 默认 desc, 如/cars?sort=-manufactorer,+model

分页

  • limit=NUM: 限制每页的数量
  • offset=NUM: 列表的偏移值. 从列表开始, 与 limit 一起可以对长列表进行翻页
  • marker=ID : 上一页的最后一个元素的 id.
  • page_reverse : 上一页(true), 下一页(false)

例如 /cars?offset=10&limit=5

可选提供:1)可以在 HTTP 消息头中增加 X-Total-Count 表示总数。2)对前一页和后一页,首页、最后一页的链接。

过滤

  • fields=field : 只显示指定的 field, 如 fields=id&fields=name 或 time < 1000234, 显示 id 与 name

选择

  • fields=field1,field2:只显示特定的字段。 如 /cars?fields=manufacturer,model,id,color

范围

  • field=[begin, end]:指定 field 的范围为 beign 和 end 之间。比如 /cars?createAt=[2000-10-11, 2010-10-12]

基于以上参数提供 AND 的语义, 对于 OR 语义, 可选支持(客户端可以将多个操作合并, 间接实现 OR 语义)

limit=2&marker=1234&page_reverse=false

/v2.0/networks.json?limit=2&page_reverse=false HTTP/1.1

{
    "networks": [
        {
            "admin_state_up": true,
            "id": "396f12f8-521e-4b91-8e21-2e003500433a",
            "name": "net3",
            "provider:network_type": "vlan",
            "provider:physical_network": "physnet1",
            "provider:segmentation_id": 1002,
            "router:external": false,
            "shared": false,
            "status": "ACTIVE",
            "subnets": [],
            "tenant_id": "20bd52ff3e1b40039c312395b04683cf"
            "project_id": "20bd52ff3e1b40039c312395b04683cf"
        },
        {
            "admin_state_up": true,
            "id": "71c1e68c-171a-4aa2-aca5-50ea153a3718",
            "name": "net2",
            "provider:network_type": "vlan",
            "provider:physical_network": "physnet1",
            "provider:segmentation_id": 1001,
            "router:external": false,
            "shared": false,
            "status": "ACTIVE",
            "subnets": [],
            "tenant_id": "20bd52ff3e1b40039c312395b04683cf"
            "project_id": "20bd52ff3e1b40039c312395b04683cf"
        }
    ],
    "networks_links": [
        {
            "href": "http://127.0.0.1:9696/v2.0/networks.json?limit=2&marker=71c1e68c-171a-4aa2-aca5-50ea153a3718",
            "rel": "next"
        },
        {
            "href": "http://127.0.0.1:9696/v2.0/networks.json?limit=2&marker=396f12f8-521e-4b91-8e21-2e003500433a&page_reverse=True",
            "rel": "previous"
        }
    ]
}

其中 networks_links 中指明下一页的链接, 上一页的链接. 最后一页没有 next 链接,
第一页没有 previous 链接

URL 范例

GET / : 列出所有版本

{
    "versions": [
        {
            "status": "CURRENT",
            "updated": "2012-01-04T11:33:21Z",
            "id": "v1.0",
            "links": [
                {
                    "href": "http://23.253.228.211:8776/v1/",
                    "rel": "self"
                }
            ]
        },
        {
            "status": "CURRENT",
            "updated": "2012-11-21T11:33:21Z",
            "id": "v2.0",
            "links": [
                {
                    "href": "http://23.253.228.211:8776/v2/",
                    "rel": "self"
                }
            ]
        }
    ]
}

其中 updated 是可选的

GET /v3 : 列出 v3 信息

{
    "version": {
        "id": "v3.4",
        "links": [
            {
                "href": "http://localhost:35357/v3/",
                "rel": "self"
            }
        ],
        "media-types": [
            {
                "base": "application/json",
                "type": "application/vnd.openstack.identity-v3+json"
            }
        ],
        "status": "stable",
        "updated": "2015-03-30T00:00:00Z"
    }
}

应答

应答体中应该但不仅限于如下字段

  • id string : UUID
  • self string : 当前资源的 url 路径
  • name string : 名称
  • description string : 对该资源的描述
  • status string : 资源的状态
  • created_at string : 资源上次更新时间, 格式参考如下
  • updated_at string : 资源创建时间, 格式 IOS8601 即 CCYY-MM-DDThh:mm:ss±hh:mm
  • schema string : 建议该 json 文档的 schema
  • fail_reason string : 失败原因
  • links array :

时间格式

格式为 IOS8601
即 CCYY-MM-DDThh:mm:ss±hh:mm, 例如 2015-08-27T09:49:58-05:00.
如果包含 ±hh:mm 表示相对 UTC 的偏移

注:

应答体 key 中单词统一为小写, 如果是多个单词组成, 以 “_” 分割. 如 created_at, updated_at 等
应答体 value 中单词统一为小写, 如果是多个单词组成, 以 “-” 分割.

对应 POST 创建资源, 返回体中必须包含资源的 id

例子

GET /v2/images

{
    "images": [
        {
            "status": "active",
            "name": "cirros-0.3.2-x86_64-disk",
            "created_at": "2014-11-07T17:07:06Z",
            "updated_at": "2014-11-07T17:19:09Z",
            "visibility": "public",
            "self": "/v2/images/1bea47ed-f6a9-463b-b423-14b9cca9ad27",
            "id": "1bea47ed-f6a9-463b-b423-14b9cca9ad27",
            "schema": "/v2/schemas/image"
        },
        {
            "status": "active",
            "name": "F17-x86_64-cfntools",
            "created_at": "2014-10-30T08:23:39Z",
            "updated_at": "2014-11-03T16:40:10Z",
            "visibility": "public",
            "self": "/v2/images/781b3762-9469-4cec-b58d-3349e5de4e9c",
            "id": "781b3762-9469-4cec-b58d-3349e5de4e9c",
            "schema": "/v2/schemas/image"
        }
    ],
    "schema": "/v2/schemas/images",
    "first": "/v2/images"
}

GET /v2/images/{image_id}

{
    "image": {
        "checksum": "eb9139e4942121f22bbc2afc0400b2a4",
        "container_format": "bare",
        "created_at": "2016-03-15T15:09:07.000000",
        "deleted": false,
        "deleted_at": null,
        "disk_format": "vmdk",
        "id": "1086fa65-8c63-4081-9a0a-ddf7e88e485b",
        "is_public": false,
        "min_disk": 22,
        "min_ram": 11,
        "name": "Silas Marner",
        "owner": "c60b1d57c5034e0d86902aedf8c49be0",
        "properties": {
            "foo": "bar",
            "qe_status": "approved"
        },
        "protected": false,
        "size": 25165824,
        "status": "active",
        "updated_at": "2016-05-10T21:14:04.000000",
        "virtual_size": null
    }
}

注: images 的 value 为数组, 而不是直接数组

同步或异步

同步接口

  1. 返回 201 Created
  2. 返回 Location 头指向创建的资源位置
  3. 返回创建的资源

异步接口

  1. 返回 202 Accepted
  2. 如果能确定资源位置,Location 头指向创建资源 ;如果不能确定资源位置,提供一个可用查看异步资源的操作

批量操作

与单个接口不同之处 1. 仅仅指定多个对象; 2. 要不全部成功, 要不全部失败

HTTP 状态码

GET 获取资源 正常返回 200
DELETE 删除资源 正常返回 204
POST 创建资源 正常返回 202 (异步接口)201(同步接口)

401 Unauthorized : 没有认证
403 Forbidden : 没有权限进行操作
404 Not Found : 没有找到对应的 URL 或资源
405 : 不支持该操作
409 Conflict : 冲突

500 Internal server error
503 Service unavailable

API 文档要求

  1. URL
  2. 可能的 URL 参数
  3. 请求 body
  4. 应答 body
  5. 正常返回状态, 错误返回状态
  6. 例子, 包括正常, 异常

多对象之间关联

通过 id 相互关联

GET /v2.0/fw/firewall_rules/{firewall_rule_id}

{
    "firewall_rule": {
        "action": "allow",
        "description": "",
        "destination_ip_address": null,
        "destination_port": "80",
        "enabled": true,
        "firewall_policy_id": null,
        "id": "8722e0e0-9cc9-4490-9660-8c9a5732fbb0",
        "ip_version": 4,
        "name": "ALLOW_HTTP",
        "position": null,
        "protocol": "tcp",
        "shared": false,
        "source_ip_address": null,
        "source_port": null,
        "project_id": "45977fa2dbd7482098dd68d0d8970117",
        "tenant_id": "45977fa2dbd7482098dd68d0d8970117"
    }
}

参考

https://developer.openstack.org/api-ref/block-storage/v3/index.html?expanded=list-backups-with-detail-detail
https://docs.atlassian.com/jira/REST/cloud/#api/2/worklog-getWorklogsForIds
http://json-schema.org/latest/json-schema-hypermedia.html

附录

命名参考

请求体或应答体常用字段

size : 表示大小
count : 表示数量
status : 表示状态, 状态变化 creating, available, in-use, deleting, error(error when create), error_deleting
type : 表示类型
xx_type : 表示某个资源的类型
is_xxx: 对二义性资源的描述, 比如 is_public, is_active
percentage : 百分比
xx_items : 列表资源
xx_ips : 多个资源时, 名字加 “s”
xxx_bytes : 表示某个对象或资源大小, 建议增加单位
metadata : 元数据
xxx_format : 格式
visibility : public, private, shared, or community
expires_at : 过期时间, 参考 created_at
xxx_properties : 属性列表
admin_state_up : 管理状态 true, false
enabled : 某条规则生效 true, false
min_xxxx : 最小
max_xxxx : 最大

对应二义性的一定要用 true, false 而不是 1, 0

status :

active, deactived;
pending, accepted, rejected;
pending, processing, success, failure
active, down, build, error

Neturon 元素

network
subnet
port
segment
trunk : subport -> segment
floatingip : subnet, port
route : 1. 多个 subnet 之间通过路由打通; 2. 通过 NAT 访问外边网络;
subnets
subnetpools
fw : 防火墙

例子

认证

接口应该至少提供基本认知.

GET /v3/auth/tokens

X-Auth-Token : TOKEN_ID

GET /v3/auth/token=TOKEN_ID

{
    "token": {
        "expires_at": "2013-02-27T18:30:59.999999Z",
        "issued_at": "2013-02-27T16:30:59.999999Z",
        "methods": [
            "password"
        ],
        "user": {
            "domain": {
                "id": "1789d1",
                "links": {
                    "self": "http://identity:35357/v3/domains/1789d1"
                },
                "name": "example.com"
            },
            "id": "0ca8f6",
            "links": {
                "self": "http://identity:35357/v3/users/0ca8f6"
            },
            "name": "Joe"
        }
    }
}

POST 请求

POST /v3/services

{
    "service": {
        "type": "volume"
    }
}

{
    "service": {
        "enabled": true,
        "id": "686766",
        "links": {
            "self": "http://identity:5000/v3/services/686766"
        },
        "type": "volume"
    }
}

POST /v3/endpoints

{
    "endpoint": {
        "interface": "public",
        "name": "name",
        "region_id": "north",
        "url": "http://identity:35357/v3/endpoints/828384",
        "service_id": "686766"
    }
}

{
    "endpoint": {
        "id": "828384",
        "interface": "internal",
        "links": {
            "self": "http://identity:35357/v3/endpoints/828384"
        },
        "name": "the internal volume endpoint",
        "region_id": "north",
        "service_id": "686766",
        "url": "http://identity:35357/v3/endpoints/828384"
    }
}

返回的资源在 links 中

POST /v2.1/{tenant_id}/servers

{
    "server": {
        "name": "new-server-test",
        "imageRef": "http://openstack.example.com/openstack/images/70a599e0-31e7-49b7-b260-868f441e862b",
        "flavorRef": "http://openstack.example.com/openstack/flavors/1",
        "metadata": {
            "My Server Name": "Apache1"
        },
        "min_count": "2",
        "max_count": "3"
    }
}

{
    "server": {
        "adminPass": "wfksH3GTTseP",
        "id": "440cf918-3ee0-4143-b289-f63e1d2000e6",
        "links": [
            {
                "href": "http://openstack.example.com/v2.1/servers/440cf918-3ee0-4143-b289-f63e1d2000e6",
                "rel": "self"
            },
            {
                "href": "http://openstack.example.com/servers/440cf918-3ee0-4143-b289-f63e1d2000e6",
                "rel": "bookmark"
            }
        ]
    }
}

返回的资源在 links 中的另外一种形式

附录

错误消息
{
  "errors": [
    {
      "request_id": "1dc92f06-8ede-4fb4-8921-b507601fb59d",
      "code": "orchestration.create-failed",
      "status": 418,
      "title": "The Stack could not be created",
      "detail": "The Stack could not be created because of error(s) in other parts of the system.",
      "links": [
        {
          "rel": "help",
          "href": "TODO(sdague): example href"
        }
      ]
    },
    {
      "request_id": "d413ea12-dfcd-4009-8fad-229b475709f2",
      "code": "compute.scheduler.no-valid-host-found",
      "status": 403,
      "title": "No valid host found",
      "detail": "No valid host found for flavor m1.xlarge.",
      "links": [
        {
          "rel": "help",
          "href": "TODO(sdague): example href"
        }
      ]
    }
  ]
}
  1. code:用来表示某类错误,比如缺少参数等。是对HTTP Header Code的补充,开发团队可以根据自己的需要自己定义。
  2. message:错误信息的摘要,应该是对用户处理错误有用的信息。
  3. request_id:请求的id,方便开发定位发生错误的请求(可选)。
  4. host_id:服务器实例的ID,方便开发定位是哪台服务器实例发生了错误(可选)。
  5. server_time:发生错误时候的服务器时间(可选)。
  6. document:错误解决的文档(方便前端展示,可选)。
  7. 格式错误返回 400 Bad Request
  8. 请求 body 中包含请求的资源,如果不存在返回 400 Bad Request
  9. 请求 body 中包含了不期望的属性,返回 400 Bad Request
  10. 请求 query 中包含了不存在属性,返回 400 Bad Request
  11. 请求不支持的方法,在返回的 405 Method Not Allowed,增加消息头 Allow 指出支持的 HTTP 方法
HATEOAS

以超链接作为应用状态的引擎(Hypermedia as the Engine of Application State )为原则,用超链接作为 API 的导航

{
  "id": 711,
  "manufacturer": "bmw",
  "model": "X5",
  "seats": 5,
  "drivers": [
   {
    "id": "23",
    "name": "Stefan Jauker",
    "links": [
     {
     "rel": "self",
     "href": "/api/v1/drivers/23"
    }
   ]
  }
 ]
什么时候需要新的 API 版本
  1. 新增接口
  2. 数据类型改变
  3. 存在接口增加新的 HTTP方法
  4. 数据类型增加新的字段
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值