我的文章的读者可能熟悉采用 API First 方法开发微服务的想法。无数次,我意识到在开始任何开发之前描述预期的 URI 和底层对象模型的好处。
然而,在我 30 多年的技术探索历程中,我已经开始期待替代流程的现实。换句话说,我完全预料到,在某些情况下,API First 是不可能实现的。
在本文中,我想通过一个示例来说明如何让生产微服务的团队无需手动定义 openapi.json 文件就能成功地为其他人提供 OpenAPI 规范。
我还想走出自己的舒适区,不使用 Java、.NET 甚至 JavaScript 来做到这一点。
在我的大多数文章的结尾,我经常提到我的个人使命宣言:
“将时间集中在提供能够扩展知识产权价值的功能上。其他一切都可以利用框架、产品和服务。” – J. Vester
我在此使命宣言中的观点是,在努力实现更高层次的目标和目的时,我有责任充分利用自己的时间。基本上,如果我们的重点是销售更多小部件,那么我的时间应该花在寻找实现这一目标的方法上——避开现有框架、产品或服务已经解决的挑战。
我选择 Python 作为我的新微服务的编程语言。到目前为止,我为之前的文章编写的 Python 代码中有 99% 都是Stack Overflow 驱动开发(SODD) 或 ChatGPT 驱动答案的结果。显然,Python 超出了我的舒适区。
现在我已经确定了事情的现状,我想创建一个新的基于 Python 的 RESTful 微服务,该微服务符合我的个人使命宣言,同时对源语言有最少的经验。
就在那时,我发现了FastAPI 。
FastAPI 自 2018 年问世以来,一直是一个专注于使用 Python 类型提示提供 RESTful API 的框架。FastAPI 最棒的地方在于它能够自动生成 OpenAPI 3 规范,而从开发人员的角度来看,无需任何额外的努力。
对于这篇文章,我想到了一个文章 API 的想法,提供一个 RESTful API,允许消费者检索我最近发布的文章列表。
为了简单起见,我们假设给定的Article
包含以下属性:
id
– 简单、唯一的标识符属性(数字)title
– 文章标题(字符串)url
– 文章的完整 URL(字符串)year
– 文章发表的年份(数字)
文章 API 将包含以下 URI:
/articles
– 将检索文章列表/articles/{article_id}
– 将通过 id 属性检索单个文章/articles
– 添加新文章在我的终端中,我创建了一个名为 fast-api-demo 的新 Python 项目,然后执行了以下命令:
$ pip install --upgrade pip $ pip install fastapi $ pip install uvicorn
我创建了一个名为api.py
的新 Python 文件并添加了一些导入,并建立了一个app
变量:
from fastapi import FastAPI, HTTPException from pydantic import BaseModel app = FastAPI() if __name__ == "__main__": import uvicorn uvicorn.run(app, host="localhost", port=8000)
接下来,我定义了一个Article
对象来匹配 Article API 用例:
class Article(BaseModel): id: int title: str url: str year: int
建立模型后,我需要添加 URI......这实际上很容易:
# Route to add a new article @app.post("/articles") def create_article(article: Article): articles.append(article) return article # Route to get all articles @app.get("/articles") def get_articles(): return articles # Route to get a specific article by ID @app.get("/articles/{article_id}") def get_article(article_id: int): for article in articles: if article.id == article_id: return article raise HTTPException(status_code=404, detail="Article not found")
为了避免涉及外部数据存储,我决定以编程方式添加一些我最近发布的文章:
articles = [ Article(id=1, title="Distributed Cloud Architecture for Resilient Systems: Rethink Your Approach To Resilient Cloud Services", url="https://dzone.com/articles/distributed-cloud-architecture-for-resilient-syste", year=2023), Article(id=2, title="Using Unblocked to Fix a Service That Nobody Owns", url="https://dzone.com/articles/using-unblocked-to-fix-a-service-that-nobody-owns", year=2023), Article(id=3, title="Exploring the Horizon of Microservices With KubeMQ's New Control Center", url="https://dzone.com/articles/exploring-the-horizon-of-microservices-with-kubemq", year=2024), Article(id=4, title="Build a Digital Collectibles Portal Using Flow and Cadence (Part 1)", url="https://dzone.com/articles/build-a-digital-collectibles-portal-using-flow-and-1", year=2024), Article(id=5, title="Build a Flow Collectibles Portal Using Cadence (Part 2)", url="https://dzone.com/articles/build-a-flow-collectibles-portal-using-cadence-par-1", year=2024), Article(id=6, title="Eliminate Human-Based Actions With Automated Deployments: Improving Commit-to-Deploy Ratios Along the Way", url="https://dzone.com/articles/eliminate-human-based-actions-with-automated-deplo", year=2024), Article(id=7, title="Vector Tutorial: Conducting Similarity Search in Enterprise Data", url="https://dzone.com/articles/using-pgvector-to-locate-similarities-in-enterpris", year=2024), Article(id=8, title="DevSecOps: It's Time To Pay for Your Demand, Not Ingestion", url="https://dzone.com/articles/devsecops-its-time-to-pay-for-your-demand", year=2024), ]
无论你是否相信,这已经完成了 Article API 微服务的开发。
为了快速检查完整性,我在本地启动了我的 API 服务:
$ python api.py INFO: Started server process [320774] INFO: Waiting for application startup. INFO: Application startup complete. INFO: Uvicorn running on http://localhost:8000 (Press CTRL+C to quit)
然后,在另一个终端窗口中,我发送了一个 curl 请求(并将其通过管道传输到json_pp
):
$ curl localhost:8000/articles/1 | json_pp { "id": 1, "title": "Distributed Cloud Architecture for Resilient Systems: Rethink Your Approach To Resilient Cloud Services", "url": "https://dzone.com/articles/distributed-cloud-architecture-for-resilient-syste", "year": 2023 }
与其在本地运行 Article API,我还想看看部署微服务有多容易。由于我之前从未在Heroku部署过 Python 微服务,我觉得现在是尝试的好时机。
在深入研究 Heroku 之前,我需要创建一个requirements.txt
文件来描述服务的依赖关系。为此,我安装并执行了pipreqs
:
$ pip install pipreqs $ pipreqs
这为我创建了一个requirements.txt
文件,其中包含以下信息:
fastapi==0.110.1 pydantic==2.6.4 uvicorn==0.29.0
我还需要一个名为Procfile
的文件,它告诉 Heroku 如何使用uvicorn
启动我的微服务。其内容如下:
web: uvicorn api:app --host=0.0.0.0 --port=${PORT}
对于那些刚接触 Python 的人(就像我一样),我使用了《使用 Python 开始使用 Heroku》文档作为有用的指南。
由于我已经安装了 Heroku CLI,所以我只需要从我的终端登录到 Heroku 生态系统:
$ heroku login
我确保将所有更新都签入到 GitLab 上的存储库中。
接下来,可以通过以下命令使用 CLI 在 Heroku 中创建新应用程序:
$ heroku create
CLI 使用唯一的应用程序名称以及应用程序的 URL 和与应用程序关联的基于 git 的存储库进行响应:
Creating app... done, powerful-bayou-23686 https://powerful-bayou-23686-2d5be7cf118b.herokuapp.com/ | https://git.heroku.com/powerful-bayou-23686.git
请注意 - 当您阅读本文时,我的应用程序将不再在线。
看看这个。当我发出 git remote 命令时,我可以看到远程服务器已自动添加到 Heroku 生态系统中:
$ git remote heroku origin
要将fast-api-demo
应用程序部署到 Heroku,我只需使用以下命令:
$ git push heroku main
一切设置完成后,我能够在 Heroku 仪表板中验证我的新基于 Python 的服务是否已启动并正在运行:
在服务运行时,可以通过发出以下 curl 命令从 Article API 中检索id = 1
Article
:
$ curl --location 'https://powerful-bayou-23686-2d5be7cf118b.herokuapp.com/articles/1'
curl 命令返回200 OK响应和以下 JSON 负载:
{ "id": 1, "title": "Distributed Cloud Architecture for Resilient Systems: Rethink Your Approach To Resilient Cloud Services", "url": "https://dzone.com/articles/distributed-cloud-architecture-for-resilient-syste", "year": 2023 }
利用 FastAPI 的内置 OpenAPI 功能,消费者可以通过导航到自动生成的/docs
URI 来接收功能齐全的 v3 规范:
https://powerful-bayou-23686-2d5be7cf118b.herokuapp.com/docs
调用此 URL 将使用广泛采用的 Swagger UI 返回 Article API 微服务:
对于那些寻找openapi.json
文件来生成客户端以使用 Article API 的人来说,可以使用/openapi.json
URI:
https://powerful-bayou-23686-2d5be7cf118b.herokuapp.com/openapi.json
就我的示例而言,基于 JSON 的 OpenAPI v3 规范如下所示:
{ "openapi": "3.1.0", "info": { "title": "FastAPI", "version": "0.1.0" }, "paths": { "/articles": { "get": { "summary": "Get Articles", "operationId": "get_articles_articles_get", "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { } } } } } }, "post": { "summary": "Create Article", "operationId": "create_article_articles_post", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Article" } } }, "required": true }, "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { } } } }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } } }, "/articles/{article_id}": { "get": { "summary": "Get Article", "operationId": "get_article_articles__article_id__get", "parameters": [ { "name": "article_id", "in": "path", "required": true, "schema": { "type": "integer", "title": "Article Id" } } ], "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { } } } }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } } } }, "components": { "schemas": { "Article": { "properties": { "id": { "type": "integer", "title": "Id" }, "title": { "type": "string", "title": "Title" }, "url": { "type": "string", "title": "Url" }, "year": { "type": "integer", "title": "Year" } }, "type": "object", "required": [ "id", "title", "url", "year" ], "title": "Article" }, "HTTPValidationError": { "properties": { "detail": { "items": { "$ref": "#/components/schemas/ValidationError" }, "type": "array", "title": "Detail" } }, "type": "object", "title": "HTTPValidationError" }, "ValidationError": { "properties": { "loc": { "items": { "anyOf": [ { "type": "string" }, { "type": "integer" } ] }, "type": "array", "title": "Location" }, "msg": { "type": "string", "title": "Message" }, "type": { "type": "string", "title": "Error Type" } }, "type": "object", "required": [ "loc", "msg", "type" ], "title": "ValidationError" } } } }
因此,以下规范可用于通过OpenAPI Generator生成多种不同语言的客户端。
在本文开头,我已经准备好与那些不愿意使用 API First 方法的人展开斗争。从这次练习中我学到的是,像 FastAPI 这样的产品可以帮助快速定义和生成有效的 RESTful 微服务,同时还自动包含完全可用的 OpenAPI v3 规范……
事实证明,FastAPI 允许团队通过利用一个框架来产生供其他人依赖的标准化合同,从而专注于他们的目标和目的。因此,出现了另一条坚持我的个人使命宣言的道路。
在此过程中,我第一次使用 Heroku 部署基于 Python 的服务。事实证明,除了查看一些精心编写的文档外,我几乎不需要做任何努力。因此,Heroku 平台的另一个使命宣言奖励也需要提及。
如果你对本文的源代码感兴趣,可以在GitLab上找到它。
祝您度过愉快的一天!