Leon
Posted on December 15, 2021
過去曾經寫過一篇〈Apiary API 規格文件+假接口一次到位〉,簡單的說,Apiary 是幫我們產生 API 文件與假接口的工具,讓別人可以在真正的後端 API 完工前就著手開工前端的工作,而定義 Apiary 的文件是一種稱為 API Blueprint 的格式,它是以 Markdown 為基礎的擴充格式,也就是說,繞一圈回過頭來你各位終究還是要寫文件的。
問題是工程師最討厭三件事:寫註解、寫測試、寫文件。
然而:
- 註解可以不寫,因為好的程式碼是自我詮釋的
- 測試可以不寫,因為可以丟給 QA 寫 🤨
- 文件不可不寫,因為你的前端妹子同事會抓狂
於是有人發明工具,從程式碼自動產出文件,於是程式碼有了,文件也有了,接口也有了,而且比 Apiary 更棒的是,是真.接口。
通常這類的工具採用的文件規範是 OpenAPI 規範,在上手工具之前,先來快速了解一下 OpenAPI。
OpenAPI
OpenAPI 是用於描述 API 資訊的文件,包括 API 的端點、參數、輸出入格式、說明、認證等,本質上它是一個 JSON 或 YAML 文件,而文件內的 schema 則是由 OpenAPI 定義。
下面是一份 OpenAPI JSON 文件的範例:
{
"openapi": "3.0.0",
"info": {
"title": "TestAPI",
"description": "Bare en liten test",
"version": "1.0"
},
"servers": [
{
"url": "https://api.example.com/v1"
}
],
"paths": {
"/users/{userId}": {
"get": {
"summary": "Returns a user by ID",
"parameters": [
{
"name": "userId",
"in": "path",
"description": "The ID of the user to return",
"required": true,
"style": "simple",
"explode": false,
"schema": {
"type": "integer"
}
}
],
"responses": {
"200": {
"description": "Successful response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/inline_response_200"
}
}
}
},
"400": {
"description": "The specified user ID is invalid (e.g. not a number)"
},
"404": {
"description": "A user with the specified ID was not found"
}
}
}
}
},
"components": {
"schemas": {
"inline_response_200": {
"type": "object",
"properties": {
"id": {
"type": "integer"
},
"name": {
"type": "string"
}
}
}
}
}
}
裡面的 schema 有些可望文生義,剩下的不用完全懂,JSON 本來就不是人讀而是機讀的文件,有工具會幫我們產出人讀的文件。:)
有許多的工具都可以讀取 OpenAPI JSON / YAML 檔案並產出功能豐富的 web 文檔,包括:
這些工具中,最常見的應該是本家的 Swagger UI(OpenAPI 在成為開放規格以前,是 Swagger 產品線的一部分),以上面的那份範例為例,經過 Swagger UI 處理後會產出如下的 web 文檔:
我們可以看到,web 文檔告訴我們 API 的端點位址、要打哪些參數、回應的格式、錯誤回應有哪些、還可以試玩,這些內容在上面提到的工具都有,形式上大同小異罷了。
然而這一切的根源 OpenAPI JSON / YAML 終究還是要用某種手段生出來,否則無法解決不寫文件卻又不能沒有文件的需求,於是乎有人開發出從程式碼自動產出 OpenAPI JSON / YAML 的工具,例如 Python 的 FastAPI。
OpenAPI 與 FastAPI
FastAPI 是 Python 的 web 框架,它有這些特性:
- 高效能而功能豐富。通常功能越多,體積越大,效能越差,而 FastAPI 在效能與功能間取了一個很好的平衡點。
- 整合了 Pydantic 的資料驗證功能。
- 無綁定 ODM / ORM,可以自由搭配任意的 ODM 操作 MongoDB 這類的文件型資料庫,或任意的 ORM 操作 SQLite 這類的關聯資料庫。
- 整合了 OpenAPI、Swagger UI、REDOC。
- 生態健康,目前 GitHub 星星數近四萬,僅次於老牌的 Django(六萬星)與 Flask(近六萬星)。
FastAPI 極簡使用
FastAPI 是 decorator 風格的框架,我們對函式加上 FastAPI 提供的裝飾器,即可讓該函式成為一個 API 端點:
from fastapi import FastAPI
app: FastAPI = FastAPI()
@app.get(path='/items/{item_id}')
def read_item(item_id: int) -> dict:
return {'item_id': item_id}
上面的陽春 API 範例中,我們做了幾件事:
-
@app.get()
裝飾器定義了此函式read_item()
接受 HTTP 的 GET 請求 - 對外暴露的 API 端點為
/items/{item_id}
-
item_id
則在函式簽名內定義型態應為整數
在談到 OpenAPI 前,先把 pydantic 的資料驗證的特性整合進來,讓範例更接近實用一點。
FastAPI + Pydantic
沿用上面的例子,我們把 item 的真身做出來:
from fastapi import FastAPI
from pydantic import BaseModel
class Item(BaseModel):
id: int
name: str
quantity: int
app: FastAPI = FastAPI()
@app.get(path='/items/{item_id}')
def read_item(item_id: int) -> dict:
item = get_single_item_by_id(item_id)
return item
與前一個範例比起來,這邊多了幾個特性:
- 用
Item
class 定義了item
物件內的欄位與型態 -
read_item()
直接回傳item
如果要表現更多 FastAPI 對 OpenAPI 整合特性,還有更多錦上添花的部份可以摻進來。
FastAPI + OpenAPI
我們加入更多完善 OpenAPI 文檔的元素:
from fastapi import FastAPI
from pydantic import BaseModel
class Item(BaseModel):
id: int
name: str
quantity: int
app: FastAPI = FastAPI(servers=[{'url': 'http://localhost:5000'}])
@app.get(
path='/items/{item_id}',
operation_id='read_item',
response_model=Item,
)
def read_item(item_id: int) -> Item:
"""Read item by id."""
item = get_single_item_by_id(item_id)
return item
@app.delete(path='/items/{item_id}', operation_id='delete_item')
def delete_item(item_id: int) -> None:
"""Delete item by id."""
delete_single_item_by_id(item_id)
return None
相較於前一個範例,這邊又多了幾個特性:
- 建構 FastAPI 實例時加入了
servers
參數,用於在 OpenAPI 文檔內指示 API 站台位址。 - 函式裝飾器加入了
operation_id='read_item'
參數。 - 函式內加入了 docstring。
- 加入了另一個用於刪除的端點。
operationId
是 OpenAPI 用於辨識 API 端點與操作的唯一值,由於在 RESTful API 的風格下,一個 API 端點 /items/{item_id}
可能會同時接受 GET、DELETE、PUT 等動作,每種動作分別對應到內部不同的處理函式,如同範例中所展示的那樣,但這些內部函式又不為外界所知,所以用 operationId
作為唯一的識別值有其必要性。
在一番折騰下,我們終於可以看看產出的 OpenAPI JSON 檔與 web 文檔:
{
"openapi": "3.0.2",
"info": {
"title": "FastAPI",
"version": "0.1.0"
},
"servers": [
{
"url": "http://localhost:5000"
}
],
"paths": {
"/items/{item_id}": {
"get": {
"summary": "Read Item",
"description": "Read item by id.",
"operationId": "read_item",
"parameters": [
{
"required": true,
"schema": {
"title": "Item Id",
"type": "integer"
},
"name": "item_id",
"in": "path"
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Item"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
},
"delete": {
"summary": "Delete Item",
"description": "Delete item by id.",
"operationId": "delete_item",
"parameters": [
{
"required": true,
"schema": {
"title": "Item Id",
"type": "integer"
},
"name": "item_id",
"in": "path"
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"HTTPValidationError": {
"title": "HTTPValidationError",
"type": "object",
"properties": {
"detail": {
"title": "Detail",
"type": "array",
"items": {
"$ref": "#/components/schemas/ValidationError"
}
}
}
},
"Item": {
"title": "Item",
"required": [
"id",
"name",
"quantity"
],
"type": "object",
"properties": {
"id": {
"title": "Id",
"type": "integer"
},
"name": {
"title": "Name",
"type": "string"
},
"quantity": {
"title": "Quantity",
"type": "integer"
}
}
},
"ValidationError": {
"title": "ValidationError",
"required": [
"loc",
"msg",
"type"
],
"type": "object",
"properties": {
"loc": {
"title": "Location",
"type": "array",
"items": {
"type": "string"
}
},
"msg": {
"title": "Message",
"type": "string"
},
"type": {
"title": "Error Type",
"type": "string"
}
}
}
}
}
}
同樣的,我們不追求用肉身理解 OpenAPI JSON,我們用 FastAPI 自動幫我們產出的 Swagger UI 的 web 文檔來輔助我們人類理解這份 API 的用法:
在上面的總覽檢視,可以看到與程式碼互相對應的部份:
- Servers 區塊來自程式碼內的
server
參數 - 「GET
/items/{item_id}
」與「DELETE/items/{item_id}
」分別對應到程式碼內的裝飾器與函式
把「GET /items/{item_id}
」端點展開:
可以看到:
- 函式的 docstring 變成端點的說明文
-
item_id
被列為參數,並且型態必須為整數,與程式內聲明的一致 - 成功回應的 JSON 格式與我們所定義的
Item
model 一致
在 FastAPI 框架中大量運用了 Python 的 type hints 特性與 Pydantic 的型態驗證特性,在程式內的型態聲明不僅用於 OpenAPI 的文檔內,也真實的運作在端點函式內,Pydantic 會自動幫我們檢查收到的參數型態是否相符,當一個聲明應為整數的參數,卻收到其他型別時,Pydantic 會直接返回錯誤,這個特性為我們節省了大量的型態驗證工作,相較於不檢查或儲存才檢查,在更前方把不良的資料擋下是更好也更安全的策略。
回到 OpenAPI 的主題,有與 OpenAPI 做原生整合的框架當然不只 FastAPI 一個,由 Flask 維護者之一的李輝先生開發的 APIFlask 也與 OpenAPI 做了原生整合,即便未有原生整合的框架,也大都有第三方套件可以幫助產出 OpenAPI 文件。
在此之前都是談後端與 OpenAPI 如何如何,下面談談 OpenAPI 在前端的運用。
OpenAPI 在前端
除了變出美美的 web 文檔外,OpenAPI JSON / YAML 本身也是個機讀文件,裡面定義了後端 API 的 server、path、HTTP method、operation ID、parameter、schema 等前端會用到的資訊,那理所當然的也有套件可以幫助我們在前端專案用上 OpenAPI JSON / YAML,這套件就是 Swagger Client,Swagger Client 與 Swagger UI 一樣,也是祖傳正宗本家嫡系 Swagger 家族的一員。
Swagger Client 用法
一個絕簡單的使用範例:
import SwaggerClient from "swagger-client";
async function getOasClient() {
return await SwaggerClient({
url: "http://localhost:5000/openapi.json",
});
}
async function getItem(id) {
const oasClient = await getOasClient();
const response = await oasClient.execute({
operationId: "read_item",
parameters: { item_id: id },
});
return response.body
}
和用 fetch()
寫起來有變簡單嗎?好像差不多,因為:
- 不用查 HTTP 方法與端點路徑
- 但要手動從 OpenAPI JSON / YAML 查 Operation ID
- 要傳遞的參數、型態、回傳格式還是要對照 web 文檔看
沒有用了可以少奮鬥三十年的感覺…,讓我們結束這一回合。
總結
至今我們已經看過兩種 API 資訊交換格式 API Blueprint 與 OpenAPI,當然老外一向是造輪子在行的,還有其他的流派像是 RAML、AsyncAPI、AML / AMF 等,目前看來 OpenAPI 已經是最常見的標準,其他流派也或多或少的往與 OpenAPI 相容的方向走,但唯一也是最大的例外是 GraphQL,相較於走 RESTful 的 resource 為基礎的 OpenAPI,有固定的 schema,GraphQL 則是一種 Query Language,不存在固定的 schema,而是依查詢動態的回覆,兩種標準間很難做到不打折的轉換。
本文僅提到了 OpenAPI 工具的一小部份,OpenAPI JSON / YAML 能做的還有很多,API 測試、監控、假 API、安全檢測等,在此附上兩個 OpenAPI 相關工具的連結:
Posted on December 15, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
December 28, 2023