Flask --(33)REST API 设计风格

2000年,Roy Thomas Fielding博士在他的博士论文《Architectural Styles and the Design of Network-based Software Architectures》中提出了几种软件应用的架构风格,REST作为其中的一种架构风格在这篇论文中进行了概括性的介绍。

REST:Representational State Transfer的缩写,翻译:“具象状态传输”。一般解释为“表现层状态转换”。

REST是设计风格而不是标准。是指客户端和服务器的交互形式。我们需要关注的重点是如何设计REST风格的网络接口。

REST的特点:
具象的。一般指表现层,要表现的对象就是资源。比如,客户端访问服务器,获取的数据就是资源。比如文字、图片、音视频等。

表现:资源的表现形式。txt格式、html格式、json格式、jpg格式等。浏览器通过URL确定资源的位置,但是需要在HTTP请求头中,用Accept和Content-Type字段指定,这两个字段是对资源表现的描述。

状态转换:客户端和服务器交互的过程。在这个过程中,一定会有数据和状态的转化,这种转化叫做状态转换。其中,GET表示获取资源,POST表示新建资源,PUT表示更新资源,DELETE表示删除资源。HTTP协议中最常用的就是这四种操作方式。

RESTful架构:
每个URL代表一种资源;
客户端和服务器之间,传递这种资源的某种表现层;
客户端通过四个http动词,对服务器资源进行操作,实现表现层状态转换。

如何设计符合 Restful 风格的 API

域名

将 api 部署在专用的域名下:

http://api.example.com

或者将api放在主域名下:

http://www.example.com/api/
版本

将API的版本号放在url中。

http://www.example.com/app/1.0/info
http://www.example.com/app/1.2/info
路径

路径表示API的具体网址。每个网址代表一种资源。 资源作为网址,网址中不能有动词只能有名词,一般名词要与数据库的表名对应。而且名词要使用复数。
错误示例:

http://www.example.com/getGoods
http://www.example.com/listOrders

正确示例:

#获取单个商品

http://www.example.com/app/goods/1
#获取所有商品

http://www.example.com/app/goods
使用标准的HTTP方法:

对于资源的具体操作类型,由HTTP动词表示。 常用的HTTP动词有四个。

GET     SELECT :从服务器获取资源。
POST    CREATE :在服务器新建资源。
PUT     UPDATE :在服务器更新资源。
DELETE  DELETE :从服务器删除资源。

示例:

#获取指定商品的信息

GET http://www.example.com/goods/ID

#新建商品的信息

POST http://www.example.com/goods

#更新指定商品的信息

PUT http://www.example.com/goods/ID

#删除指定商品的信息

DELETE http://www.example.com/goods/ID
过滤信息

如果资源数据较多,服务器不能将所有数据一次全部返回给客户端。API应该提供参数,过滤返回结果。 实例:

#指定返回数据的数量

http://www.example.com/goods?limit=10
#指定返回数据的开始位置

http://www.example.com/goods?offset=10
#指定第几页,以及每页数据的数量

http://www.example.com/goods?page=2&per_page=20
状态码

服务器向用户返回的状态码和提示信息,常用的有:

200 OK  :服务器成功返回用户请求的数据
201 CREATED :用户新建或修改数据成功。
202 Accepted:表示请求已进入后台排队。
400 INVALID REQUEST :用户发出的请求有错误。
401 Unauthorized :用户没有权限。
403 Forbidden :访问被禁止。
404 NOT FOUND :请求针对的是不存在的记录。
406 Not Acceptable :用户请求的的格式不正确。
500 INTERNAL SERVER ERROR :服务器发生错误。
错误信息

一般来说,服务器返回的错误信息,以键值对的形式返回。

{
    error: 'Invalid API KEY'
}
响应结果

针对不同结果,服务器向客户端返回的结果应符合以下规范。
#返回商品列表

GET    http://www.example.com/goods

#返回单个商品

GET    http://www.example.com/goods/cup

#返回新生成的商品

POST   http://www.example.com/goods

#返回一个空文档

DELETE http://www.example.com/goods
使用链接关联相关的资源

在返回响应结果时提供链接其他API的方法,使客户端很方便的获取相关联的信息。

其他

服务器返回的数据格式,应该尽量使用JSON,避免使用XML。

更新于 2019.8.6

构建具有 restful 风格的 API

使用后端渲染的方式构建的 API 只适用于特定
类型的浏览器。但是如果我想构建安卓或者是 iOS APP, 有两种主流的方法可以解决这个问题:
最简单的解决方案是构建一个简单的APP,仅使用一个Web视图组件并用Microblog网站填充整个屏幕,但相比在设备的Web浏览器中打开网站,这种方案几乎没有什么卖点。
一个更好的解决方案(尽管更费力)将是构建一个本地APP,但这个APP如何与仅返回HTML页面的服务器交互呢?

这就是应用程序编程接口(API)的能力范畴了。 API是一组HTTP路由,被设计为应用程序中的低级入口点。与定义返回HTML以供Web浏览器使用的路由和视图函数不同,API允许客户端直接使用应用程序的资源,从而决定如何通过客户端完全地向用户呈现信息。 例如,Microblog中的API可以向用户提供用户信息和用户动态,并且它还可以允许用户编辑现有动态,但仅限于数据级别,不会将此逻辑与HTML混合。

REST as a Foundation of API Design

REST“纯粹主义者”认为REST API必须以非常明确的方式遵循全部六个特征,而不像REST“实用主义者”那样,仅仅将Dr. Fielding在论文中提出的想法作为指导原则或建议。Dr.Fielding站在纯粹主义阵营的一边,并在博客文章和在线评论中的撰写了一些额外的见解来表达他的愿景。
目前实施的绝大多数API都遵循“实用主义”的REST实现。 包括来自Facebook,GitHub,Twitter等“大玩家”的大部分API都是如此。很少有公共API被一致认为是纯REST,因为大多数API都没有包含纯粹主义者认为必须实现的某些细节。 尽管Dr. Fielding和其他REST纯粹主义者对评判一个API是否是REST API有严格的规定,但软件行业在实际运用中引用REST是很常见的。
为了让你了解REST论文中的内容,以下各节将介绍Dr. Fielding列举的六项原则。

客户端-服务器原则

客户端-服务器原则相当简单,正如其字面含义,在 REST API 中,客户端和服务器的角色应该明确区分。在实践中,这意味着客户端和服务器都是单独的进程,并在大多数的情况下,基于使用 TCP 网络上的 HTTP 协议进行通信。

分层系统

分层系统原则是说当客户端需要与服务器通信时,它可能最终连接到代理服务器而不是实际的服务器。因此对于客户端来说,如果不是直接连接到服务器,它发送请求的方式应该没有什么区别。事实上,它甚至可能不知道它是否连接到目标服务器。 同样,这个原则规定服务器兼容直接接收来自代理服务器的请求,所以它绝不能假设连接的另一端一定是客户端。
这是REST的一个重要特性,因为能够添加中间节点的这个特性,允许应用程序架构师使用负载均衡器,缓存,代理服务器等来设计满足大量请求的大型复杂网络。

缓存

该原则扩展了分层系统,通过明确指出允许服务器或代理服务器缓存频繁且相同请求的响应内容以提高系统性能。 有一个你可能熟悉的缓存实现:所有Web浏览器中的缓存。 Web浏览器缓存层通常用于避免一遍又一遍地请求相同的文件,例如图像。
为了达到API的目的,目标服务器需要通过使用缓存控制来指示响应是否可以在代理服务器传回客户端时进行缓存。 请注意,由于安全原因,部署到生产环境的API必须使用加密,因此,除非此代理服务器terminates SSL连接,或者执行解密和重新加密,否则缓存通常不会在代理服务器中完成。

按需获取客户端代码(Code On Demand)

这是一项可选要求,规定服务器可以提供可执行代码以响应客户端,这样一来,就可以从服务器上获取客户端的新功能。 因为这个原则需要服务器和客户端之间就客户端能够运行的可执行代码类型达成一致,所以这在API中很少使用。 你可能会认为服务器可能会返回JavaScript代码以供Web浏览器客户端执行,但REST并非专门针对Web浏览器客户端而设计。 例如,如果客户端是iOS或Android设备,执行JavaScript可能会带来一些复杂情况。

无状态

无状态原则是REST纯粹主义者和实用主义者之间争论最多的两个中心之一。 它指出,REST API不应保存客户端发送请求时的任何状态。 这意味着,在Web开发中常见的机制都不能在用户浏览应用程序页面时“记住”用户。 在无状态API中,每个请求都需要包含服务器需要识别和验证客户端并执行请求的信息。这也意味着服务器无法在数据库或其他存储形式中存储与客户端连接有关的任何数据。
如果你想知道为什么REST需要无状态服务器,主要原因是无状态服务器非常容易扩展,你只需在负载均衡器后面运行多个服务器实例即可。 如果服务器存储客户端状态,则事情会变得更复杂,因为你必须弄清楚多个服务器如何访问和更新该状态,或者确保给定客户端始终由同一服务器处理,这样的机制通常称为粘性会话。
再思考一下本章介绍中讨论的 /translate 路由,就会发现它不能被视为RESTful,因为与该路由相关的视图函数依赖于Flask-Login的@login_required装饰器, 这会将用户的登录状态存储在Flask用户会话中。

统一接口

最后,最重要的,最有争议的,最含糊不清的REST原则是统一接口。 Dr. Fielding列举了REST统一接口的四个特性:唯一资源标识符,资源表示,自描述性消息和超媒体。
唯一资源标识符是通过为每个资源分配唯一的URL来实现的。 例如,与给定用户关联的URL可以是 /api/users/ ,其中 是在数据库表主键中分配给用户的标识符。 大多数API都能很好地实现这一点。
资源表示的使用意味着当服务器和客户端交换关于资源的信息时,他们必须使用商定的格式。 对于大多数现代API,JSON格式用于构建资源表示。 API可以选择支持多种资源表示格式,并且在这种情况下,HTTP协议中的内容协商选项是客户端和服务器确认格式的机制。
自描述性消息意味着在客户端和服务器之间交换的请求和响应必须包含对方需要的所有信息。 作为一个典型的例子,HTTP请求方法用于指示客户端希望服务器执行的操作。 GET请求表示客户想要检索资源信息,POST请求表示客户想要创建新资源,PUT或PATCH请求定义对现有资源的修改,DELETE 表示删除资源的请求。 目标资源被指定为请求的URL,并在HTTP头,URL的查询字符串部分或请求主体中提供附加信息。
超媒体需求是最具争议性的,而且很少有API实现,而那些实现它的API很少以满足REST纯粹主义者的方式进行。由于应用程序中的资源都是相互关联的,因此此要求会要求将这些关系包含在资源表示中,以便客户端可以通过遍历关系来发现新资源,这几乎与你在Web应用程序中通过点击从一个页面到另一个页面的链接来发现新页面的方式相同。理想情况下,客户端可以输入一个API,而不需要任何有关其中的资源的信息,就可以简单地通过超媒体链接来了解它们。但是,与HTML和XML不同,通常用于API中资源表示的JSON格式没有定义包含链接的标准方式,因此你不得不使用自定义结构,或者类似JSON-API,HAL, JSON-LD这样的试图解决这种差距的JSON扩展之一。

举例

表示单个用户

实施API时要考虑的第一个方面是决定其资源表示形式。 我要实现一个用户类型的API,因此我需要决定的是用户资源的表示形式。 经过一番头脑风暴,得出了以下JSON表示形式:

{
    "id": 123,
    "username": "susan",
    "password": "my-password",
    "email": "susan@example.com",
    "last_seen": "2017-10-20T15:04:27Z",
    "about_me": "Hello, my name is Susan!",
    "post_count": 7,
    "follower_count": 35,
    "followed_count": 21,
    "_links": {
        "self": "/api/users/123",
        "followers": "/api/users/123/followers",
        "followed": "/api/users/123/followed",
        "avatar": "https://www.gravatar.com/avatar/..."
    }
}

许多字段是直接来自于数据库模型,post_count,follower_count和followed_count字段是“虚拟”字段,它们在数据库字段中不存在,提供给客户端是为了方便。
这说明了资源不需要和服务器中资源的实际定义一致

_links部分,它实现了超媒体要求。 定义的链接包括指向当前资源的链接,用户的粉丝列表链接,用户关注的用户列表链接,最后是指向用户头像图像的链接。 将来,如果我决定向这个API添加用户动态,那么用户的动态列表链接也应包含在这里。

JSON格式的一个好处是,它总是转换为Python字典或列表的表示形式。 Python标准库中的json包负责Python数据结构和JSON之间的转换。因此,为了生成这些表示,我将在User模型中添加一个名为to_dict()的方法,该方法返回一个Python字典。

例如:

from flask import url_for
# ...

class User(UserMixin, db.Model):
    # ...

    def to_dict(self, include_email=False):
        data = {
            'id': self.id,
            'username': self.username,
            'last_seen': self.last_seen.isoformat() + 'Z',
            'about_me': self.about_me,
            'post_count': self.posts.count(),
            'follower_count': self.followers.count(),
            'followed_count': self.followed.count(),
            '_links': {
                'self': url_for('api.get_user', id=self.id),
                'followers': url_for('api.get_followers', id=self.id),
                'followed': url_for('api.get_followed', id=self.id),
                'avatar': self.avatar(128)
            }
        }
        if include_email:
            data['email'] = self.email
        return data

同时使用 from_dict 将用户对象转换为 python 表示:

class User(UserMixin, db.Model):
    # ...

    def from_dict(self, data, new_user=False):
        for field in ['username', 'email', 'about_me']:
            if field in data:
                setattr(self, field, data[field])
        if new_user and 'password' in data:
            self.set_password(data['password'])
表示用户集合

除了使用单个资源表示形式外,此API还需要一组用户的表示。 例如客户请求用户或粉丝列表时使用的格式。 以下是一组用户的表示:

{
    "items": [
        { ... user resource ... },
        { ... user resource ... },
        ...
    ],
    "_meta": {
        "page": 1,
        "per_page": 10,
        "total_pages": 20,
        "total_items": 195
    },
    "_links": {
        "self": "http://localhost:5000/api/users?page=1",
        "next": "http://localhost:5000/api/users?page=2",
        "prev": null
    }
}

在这个表示中,items是用户资源的列表,每个用户资源的定义如前一节所述。 _meta部分包含集合的元数据,客户端在向用户渲染分页控件时就会用得上。 _links部分定义了相关链接,包括集合本身的链接以及上一页和下一页链接,也能帮助客户端对列表进行分页。

分页逻辑的实现

由于分页逻辑,生成用户集合的表示很棘手,但是该逻辑对于我将来可能要添加到此API的其他资源来说是一致的,所以我将以通用的方式实现它,以便适用于其他模型。
就像对于全文索引,使用实现一个SearchableMixin类,任何需要全文索引的模型都可以从中继承一样,我们可以实现一个新的mixin类,命名为PaginatedAPIMixin。任何需要分页的模型都可以继承它:

class PaginatedAPIMixin(object):
    @staticmethod
    def to_collection_dict(query, page, per_page, endpoint, **kwargs):
        resources = query.paginate(page, per_page, False)
        data = {
            'items': [item.to_dict() for item in resources.items],
            '_meta': {
                'page': page,
                'per_page': per_page,
                'total_pages': resources.pages,
                'total_items': resources.total
            },
            '_links': {
                'self': url_for(endpoint, page=page, per_page=per_page,
                                **kwargs),
                'next': url_for(endpoint, page=page + 1, per_page=per_page,
                                **kwargs) if resources.has_next else None,
                'prev': url_for(endpoint, page=page - 1, per_page=per_page,
                                **kwargs) if resources.has_prev else None
            }
        }
        return data

to_collection_dict()方法产生一个带有用户集合表示的字典,包括items,_meta和_links部分。 你可能需要仔细检查该方法以了解其工作原理。 前三个参数是Flask-SQLAlchemy查询对象,页码和每页数据数量。 这些是决定要返回的条目是什么的参数。 该实现使用查询对象的paginate()方法来获取该页的条目,就像我对主页,发现页和个人主页中的用户动态所做的一样。

复杂的部分是生成链接,其中包括自引用以及指向下一页和上一页的链接。 我想让这个函数具有通用性,所以我不能使用类似url_for(‘api.get_users’, id=id, page=page)这样的代码来生成自链接(译者注:因为这样就固定成用户资源专用了)。 url_for()的参数将取决于特定的资源集合,所以我将依赖于调用者在endpoint参数中传递的值,来确定需要发送到url_for()的视图函数。 由于许多路由都需要参数,我还需要在kwargs中捕获更多关键字参数,并将它们传递给url_for()。 page和per_page查询字符串参数是明确给出的,因为它们控制所有API路由的分页。

错误处理

定义错误页面仅适用于使用Web浏览器的用户。当一个API需要返回一个错误时,它需要是一个“机器友好”的错误类型,以便客户端可以轻松解释这些错误。 因此,我同样设计错误的表示为一个JSON。 以下是我要使用的基本结构:

{
    "error": "short error description",
    "message": "error message (optional)"
}

除了错误的有效负荷之外, 我还会使用HTTP协议的状态代码来指示常见错误的类型。 为了帮助我生成这些错误响应,我将在app/api/errors.py中写入error_response()函数:

from flask import jsonify
from werkzeug.http import HTTP_STATUS_CODES

def error_response(status_code, message=None):
    payload = {'error': HTTP_STATUS_CODES.get(status_code, 'Unknown error')}
    if message:
        payload['message'] = message
    response = jsonify(payload)
    response.status_code = status_code
    return response

该函数使用来自Werkzeug(Flask的核心依赖项)的HTTP_STATUS_CODES字典,它为每个HTTP状态代码提供一个简短的描述性名称。 我在错误表示中使用这些名称作为error字段的值,所以我只需要操心数字状态码和可选的长描述。 jsonify()函数返回一个默认状态码为200的FlaskResponse对象,因此在创建响应之后,我将状态码设置为对应的错误代码。

API将返回的最常见错误将是代码400,代表了“错误的请求”。 这是客户端发送请求中包含无效数据的错误。 为了更容易产生这个错误,我将为它添加一个专用函数,只需传入长的描述性消息作为参数就可以调用。 下面是我之前添加的bad_request()占位符:
app/api/errors.py:错误请求的响应。

# ...

def bad_request(message):
    return error_response(400, message)
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值