Flask API开发的最佳实践

732 阅读12分钟

Python是我最喜欢的编程语言。它的适应性、可读性和编码速度都很独特,使Python成为各种项目的有力选择,从数据科学项目到脚本,当然也包括API。

Python是API开发的热门选择,不仅因为它是最受喜爱的编程语言之一,还因为它有丰富的库和框架生态系统为这个目标服务,这些库有巨大的知名度,如DjangoFlaskFastAPI

但是你应该用哪个框架来构建你的Python的API?这100%取决于你,但有一些重要的考虑因素需要记住。毕竟,这些框架中的一些是不同的,甚至是来自于意识形态的不同。

  • Django是一个包罗万象的框架。它提供了处理API请求、序列化、数据库连接、自动生成管理界面等的工具和模块。
  • 相反,Flask是一个简约的框架,它只提供必要的工具,但它通过额外的库和框架来扩展其功能。最重要的是,你完全可以决定你的项目需要什么,仅此而已。
  • FastAPI是一个相对较新的框架。它利用了较新的python特性,如类型提示、并发处理(有async),而且速度超快。

我经常使用Flask和FastAPI,我很喜欢这两者。我喜欢这些框架的灵活性和适应性,在今天的文章中,我们将重点讨论Flask。

下面的提示和实践是研究的结果,也是八年多来用Python构建和运送生产级API的经验。

  • 用正确的名称和HTTP动词设计你的API端点

  • 如何正确构建你的应用程序

  • 从代码中构建你的文档

  • 测试

让我们开始吧!🚀

用正确的名称和HTTP动词设计你的API端点

一个充分设计的API对于开发者来说是很容易理解的,也是很直接的。通过阅读URI和HTTP动词(后面会有更多介绍),开发人员可以很好地理解在调用某个特定方法时应该发生什么。

但这是如何实现的呢?让我们从命名URI开始。在REST中,我们把Resource ,以第一层的数据表示。在你的API中一致地命名这些资源将变成长期的最佳决定之一。

请注意,我在前一句话中强调了一致性,因为这是一个关键因素。当然,有一些特别的方法来命名你的资源,我们将介绍这些方法,但对于你选择的实际约定来说,一致是更重要的。

让我们通过模拟一个有客户、订单和结账过程的简单电子商务网站来开始实践。

我们的主要资源是customers ,它是实例customer 的一个集合。有了这些信息,我们可以通过URI来识别集合资源 /customers或通过URI来识别单个资源 /customers/{customerId}.随后,我们可以识别子资源,如orders ,我们可以将其识别为 /customers/{customerId}/orders,或者通过一个单一的订单资源 /customers/{customerId}/orders/{orderId}.

命名资源的最佳实践

  1. 使用名词的复数形式来表示资源,例如:使用"-"来表示。
    • ✅一个系统的用户。 /users, /users/{userId}
    • ✅ 用户的播放列表。 /users/{userId}/playlists, /users/{userId}/playlists/{playlistId}
  2. 使用连字符"-"来分隔单词并提高可兑换性。
    • /users/{userId}/-mobile-devices
    • /users/{userId}/mobileDevices
    • /users/{userId}/mobile_devices
  3. 使用正斜线"/"来表示层次结构
    • /users/{userId}/mobile-devices
    • /users-mobile-devices/{userId}
    • /users-mobile-devices/?userId={userId}
  4. 在URI中只使用小写字母
    • /users/{userId}/mobile-devices
    • /Users/{userId}/Mobile-Devices

现在我们了解了如何命名资源,我们需要考虑行动。在我们的API中,有一些方法本质上是程序性的,与特定的资源无关,例如:结账、运行、播放等。

命名行动的最佳做法

  1. 使用动词来表示行动,例如。
    • ✅ 执行一个结账动作。 /users/{userId}/cart/checkout
  2. 与资源一样,使用连字符、正斜线和小写字母。

这里的一个关键点是要区分CRUD函数和动作,因为两者都是动作。在REST中,CRUD操作,如创建、读取、更新和删除,是通过HTTP动词而不是URI来处理的。

但什么是HTTP动词或HTTP请求方法?

HTTP定义了一组请求方法,以表示对资源进行的操作(听起来很熟悉吧)。该列表包括几个,但我们将专注于5个。

  • GET:应该是用于数据检索的。
  • POST:应该用来创建一个新的资源。
  • PUT:应该用于更新特定资源的信息。
  • DELETE:应该用来删除一个特定的资源。
  • PATCH:应该用于更新特定资源的部分信息。

我们的电子商务网站的例子

  • GET/users:所有用户的列表。
  • POST/users:创建一个新的用户。
  • PUT/users/{userId}: 更新一个用户。
  • DELETE/users/{userId}:删除一个特定的用户。
  • PATCH/users/{userId}:部分地更新一个用户。
  • GET/users/{userId}/orders列表:某个特定用户的所有订单。
  • POST/users/{userId}/cart/checkout:运行结账过程。

你不应该做什么。

  • /users/get-all
  • /users/create
  • /users/{userId}/list-orders

以任何形式的GET、POST或其他动词。

如何正确地构建你的应用程序

我想在这一节开始说,没有一个正确的方法来结构你的应用程序,这取决于应用程序的大小、模块、要求、甚至个人的喜好。这可能会有所不同。不过,我想向你介绍一下我的团队是如何结构Flask应用程序的,我们在多个生产项目中使用了这种设置。

你可以按照文章中对结构的解释,你也可以在github上的Flask API入门套件中找到这个结构,可以随时使用。

project/
    api/
        model/
            __init__.py
            welcome.py
        route/
            home.py
        schema/
            __init__.py
            welcome.py
        service
            __init__.py
            welcome.py

    test/
        route/
            __init__.py
            test_home.py
        __init.py

    .gitignore
    app.py
    Pipfile
    Pipfile.lock

现在让我们把它分解开来,解释每个模块。

所有的应用魔法都发生在API模块(/api),在那里,我们把代码分成4个主要部分。

  • models 是我们应用程序的数据描述符,在很多情况下与数据库模型有关。每个模型如何定义将在很大程度上取决于你用来连接数据库的库。
  • routes 是我们应用程序的URI,我们在这里定义我们的资源和动作。
  • schemas 是对我们的API的输入和输出的定义,允许哪些参数,我们将输出哪些信息。它们与我们的资源相关联,但它们不一定与我们的模型相同。
  • services 是定义应用逻辑或与其他服务或db层交互的模块。路由应该尽可能的简单,并将所有的逻辑委托给服务。

Flask中的每个端点都可以单独定义,也可以通过称为蓝图的组来定义。在我的例子中,我喜欢蓝图提供的分组,我为每个资源使用它们。让我们来看看我们的欢迎路线的一个例子 (./api/route/home.py)会是什么样子。

from http import HTTPStatus
from flask import Blueprint
from flasgger import swag_from
from api.model.welcome import WelcomeModel
from api.schema.welcome import WelcomeSchema

home_api = Blueprint('api', __name__)


@home_api.route('/')
@swag_from({
    'responses': {
        HTTPStatus.OK.value: {
            'description': 'Welcome to the Flask Starter Kit',
            'schema': WelcomeSchema
        }
    }
})
def welcome():
    """
    1 liner about the route
    A more detailed description of the endpoint
    ---
    """
    result = WelcomeModel()
    return WelcomeSchema().dump(result), 200

让我们把所有的东西分成3块。

home_api = Blueprint('api', __name__)

这里是我们声明蓝图的地方,随后我们可以用它来声明我们的端点或路由。在这种情况下,我们的分组是非常基本的,但我们可以用分组做得更多,比如定义前缀、资源文件夹等等。

例如,如果我们想让我们的home 蓝图总是作为一个嵌套的路由的 /home-service,我们可以这样做。

home_api = Blueprint('api', __name__, url_prefix='/home-service')

接下来我们声明一个路由,但我们把它分成两部分。

@home_api.route('/')
@swag_from({
    'responses': {
        HTTPStatus.OK.value: {
            'description': 'Welcome to the Flask Starter Kit',
            'schema': WelcomeSchema
        }
    }
})

我们在函数之上使用注解,将其转换为端点,并提供额外的信息,例如,文档信息,下一节会有更多介绍。

最后是我们的路由代码,它只是一个Python函数。

def welcome():
    """
    1 liner about the route
    A more detailed description of the endpoint
    ---
    """
    result = WelcomeModel()
    return WelcomeSchema().dump(result), 200

注意,我们不是简单地直接返回一个字符串或JSON对象,而是使用我们的模式。在我们的例子中,我使用flask-marshmallow序列化库来实现其目的。

从代码中构建你的文档

你建立了你的API,你把它运到了生产中,开发人员渴望使用它,但他们如何知道有哪些端点可用,如何使用它们?简单的答案就是阅读文档。

文档可以通过两种方式建立,你可以打开一个编辑器,"手动 "编写文档,或者你可以使用代码来生成文档。如果你喜欢自动文档的想法,你会喜欢Swagger

Swagger是一个开源的规范,它允许你描述你的API的每个元素,这样任何机器或系统都可以解释它并与之互动。由于这个规范,许多工具被开发出来,以提供丰富的接口,使我们的文档变得动态和互动,同时也为开发人员提供了轻松生成这些swagger文件的工具。

对于Flask来说,有多个用于自动生成Swagger的库,但我最喜欢的是flasgger。Flassger提供了注释和其他工具来生成你的文档,它还提供了一个漂亮的网络界面,你可以看到每个端点、它的输入和输出,甚至可以直接从文档中运行端点。

下面是它的一张运行图片。

Swagger demo page

它是高度可配置的,并且通过使用一个额外的叫做apispec的库与我们的序列化库兼容。这一切都很容易设置,但你也可以利用Flask的入门套件,你将为你完成这一切。

但是,一旦你有了它的运行,那么文档的信息取自哪里呢?来自两个地方。

  • 还记得我们的 swag_from 函数注解吗?在那里我们可以提供关于输入和输出的详细信息

      @swag_from({
          'responses': {
              HTTPStatus.OK.value: {
                  'description': 'Welcome to the Flask Starter Kit',
                  'schema': WelcomeSchema
              }
          }
      })
    
  • 我们还可以在函数中使用字符串字面,为端点提供描述,类似于我们在这里做的。

      def welcome():
          """
          1 liner about the route
          A more detailed description of the endpoint
          ---
          """
    

还有更多的选项和定制;在他们的官方文档中都有很好的记录。

测试

如果你像我一样,也许你讨厌写测试,但如果你像我一样,你知道这是值得的。测试,如果做得好的话,从长远来看可以提高效率和质量。在对现有系统进行修改、重构或构建新功能时,它们还能让开发人员放心。

建立测试不应该太难,它应该在开发过程中自然发生。我过去在这方面做了很多努力,因为我总是先开发功能、端点或函数,然后再写测试,只是为了完成它。

我并不是说这种方法是错误的,但有一种更好的方法。TDD,即测试驱动开发,这是一个概念,你先写测试,然后再写我们要测试的实际代码。

它是如何工作的呢?让我们假设我们需要写一个函数,将两个数字相加并返回结果;令人兴奋,对吗?

通过TDD,我们的方法是先写测试。

def test_answer():
    assert sum_two_numbers(3, 5) == 8

接下来,我们运行测试,结果失败了,因为我们的函数甚至还不存在。所以接下来,我们写我们的函数。

def sum_two_numbers(num1, num2):
    return num1 * num2

接下来,我们重新运行我们的测试,但还是失败了。我们的断言失败了,但是为什么?事实证明,我犯了一个简单的错误。像我这样笨拙的人,我放了一个*,而不是一个+;如果没有我们的测试,就很难注意到这一点,但感谢上帝,我们有测试。

我们修复了我们的函数,现在一切都运行得很好。

def sum_two_numbers(num1, num2):
    return num1 + num2

在我们所做的练习中,这听起来有点傻,但对于更复杂的函数和代码来说,错误是会发生的,先有测试会有很大的帮助;我这么说是有经验的。

结论

对于不同的框架、要解决的问题、甚至人来说,最佳实践可能是不同的,没有一个正确的方法,这是我喜欢的编程方式。然而,在设计和开发API时,有基本的原则可以依靠,可以帮助你的团队和其他开发者消费你的API产品。

在命名上保持一致,在项目中的模块或文件夹中分离概念,直接从你的代码中编写文档,以及适当的测试,这些只是一些例子,可以使你的生活更容易,更有成效,并使你达到更高的水平。

我希望你喜欢阅读这篇文章!


© 2013-2021 Auth0公司。保留所有权利。