Clients generieren¶
Da FastAPI auf der OpenAPI-Spezifikation basiert, erhalten Sie automatische Kompatibilität mit vielen Tools, einschließlich der automatischen API-Dokumentation (bereitgestellt von Swagger UI).
Ein besonderer Vorteil, der nicht unbedingt offensichtlich ist, besteht darin, dass Sie für Ihre API Clients generieren können (manchmal auch SDKs genannt), für viele verschiedene Programmiersprachen.
OpenAPI-Client-Generatoren¶
Es gibt viele Tools zum Generieren von Clients aus OpenAPI.
Ein gängiges Tool ist OpenAPI Generator.
Wenn Sie ein Frontend erstellen, ist openapi-typescript-codegen eine sehr interessante Alternative.
Client- und SDK-Generatoren – Sponsor¶
Es gibt auch einige vom Unternehmen entwickelte Client- und SDK-Generatoren, die auf OpenAPI (FastAPI) basieren. In einigen Fällen können diese Ihnen weitere Funktionalität zusätzlich zu qualitativ hochwertigen generierten SDKs/Clients bieten.
Einige von diesen ✨ sponsern FastAPI ✨, das gewährleistet die kontinuierliche und gesunde Entwicklung von FastAPI und seinem Ökosystem.
Und es zeigt deren wahres Engagement für FastAPI und seine Community (Sie), da diese Ihnen nicht nur einen guten Service bieten möchten, sondern auch sicherstellen möchten, dass Sie über ein gutes und gesundes Framework verfügen, FastAPI. 🙇
Beispielsweise könnten Sie Speakeasy ausprobieren.
Es gibt auch mehrere andere Unternehmen, welche ähnliche Dienste anbieten und die Sie online suchen und finden können. 🤓
Einen TypeScript-Frontend-Client generieren¶
Beginnen wir mit einer einfachen FastAPI-Anwendung:
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
price: float
class ResponseMessage(BaseModel):
message: str
@app.post("/items/", response_model=ResponseMessage)
async def create_item(item: Item):
return {"message": "item received"}
@app.get("/items/", response_model=list[Item])
async def get_items():
return [
{"name": "Plumbus", "price": 3},
{"name": "Portal Gun", "price": 9001},
]
from typing import List
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
price: float
class ResponseMessage(BaseModel):
message: str
@app.post("/items/", response_model=ResponseMessage)
async def create_item(item: Item):
return {"message": "item received"}
@app.get("/items/", response_model=List[Item])
async def get_items():
return [
{"name": "Plumbus", "price": 3},
{"name": "Portal Gun", "price": 9001},
]
Beachten Sie, dass die Pfadoperationen die Modelle definieren, welche diese für die Request- und Response-Payload verwenden, indem sie die Modelle Item
und ResponseMessage
verwenden.
API-Dokumentation¶
Wenn Sie zur API-Dokumentation gehen, werden Sie sehen, dass diese die Schemas für die Daten enthält, welche in Requests gesendet und in Responses empfangen werden:
Sie können diese Schemas sehen, da sie mit den Modellen in der Anwendung deklariert wurden.
Diese Informationen sind im OpenAPI-Schema der Anwendung verfügbar und werden dann in der API-Dokumentation angezeigt (von Swagger UI).
Und dieselben Informationen aus den Modellen, die in OpenAPI enthalten sind, können zum Generieren des Client-Codes verwendet werden.
Einen TypeScript-Client generieren¶
Nachdem wir nun die Anwendung mit den Modellen haben, können wir den Client-Code für das Frontend generieren.
openapi-typescript-codegen
installieren¶
Sie können openapi-typescript-codegen
in Ihrem Frontend-Code installieren mit:
$ npm install openapi-typescript-codegen --save-dev
---> 100%
Client-Code generieren¶
Um den Client-Code zu generieren, können Sie das Kommandozeilentool openapi
verwenden, das soeben installiert wurde.
Da es im lokalen Projekt installiert ist, könnten Sie diesen Befehl wahrscheinlich nicht direkt aufrufen, sondern würden ihn in Ihre Datei package.json
einfügen.
Diese könnte so aussehen:
{
"name": "frontend-app",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"generate-client": "openapi --input http://localhost:8000/openapi.json --output ./src/client --client axios --useOptions --useUnionTypes"
},
"author": "",
"license": "",
"devDependencies": {
"openapi-typescript-codegen": "^0.20.1",
"typescript": "^4.6.2"
}
}
Nachdem Sie das NPM-Skript generate-client
dort stehen haben, können Sie es ausführen mit:
$ npm run generate-client
frontend-app@1.0.0 generate-client /home/user/code/frontend-app
> openapi --input http://localhost:8000/openapi.json --output ./src/client --client axios --useOptions --useUnionTypes
Dieser Befehl generiert Code in ./src/client
und verwendet intern axios
(die Frontend-HTTP-Bibliothek).
Den Client-Code ausprobieren¶
Jetzt können Sie den Client-Code importieren und verwenden. Er könnte wie folgt aussehen, beachten Sie, dass Sie automatische Codevervollständigung für die Methoden erhalten:
Sie erhalten außerdem automatische Vervollständigung für die zu sendende Payload:
Tipp
Beachten Sie die automatische Vervollständigung für name
und price
, welche in der FastAPI-Anwendung im Item
-Modell definiert wurden.
Sie erhalten Inline-Fehlerberichte für die von Ihnen gesendeten Daten:
Das Response-Objekt hat auch automatische Vervollständigung:
FastAPI-Anwendung mit Tags¶
In vielen Fällen wird Ihre FastAPI-Anwendung größer sein und Sie werden wahrscheinlich Tags verwenden, um verschiedene Gruppen von Pfadoperationen zu separieren.
Beispielsweise könnten Sie einen Abschnitt für Items (Artikel) und einen weiteren Abschnitt für Users (Benutzer) haben, und diese könnten durch Tags getrennt sein:
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
price: float
class ResponseMessage(BaseModel):
message: str
class User(BaseModel):
username: str
email: str
@app.post("/items/", response_model=ResponseMessage, tags=["items"])
async def create_item(item: Item):
return {"message": "Item received"}
@app.get("/items/", response_model=list[Item], tags=["items"])
async def get_items():
return [
{"name": "Plumbus", "price": 3},
{"name": "Portal Gun", "price": 9001},
]
@app.post("/users/", response_model=ResponseMessage, tags=["users"])
async def create_user(user: User):
return {"message": "User received"}
from typing import List
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
price: float
class ResponseMessage(BaseModel):
message: str
class User(BaseModel):
username: str
email: str
@app.post("/items/", response_model=ResponseMessage, tags=["items"])
async def create_item(item: Item):
return {"message": "Item received"}
@app.get("/items/", response_model=List[Item], tags=["items"])
async def get_items():
return [
{"name": "Plumbus", "price": 3},
{"name": "Portal Gun", "price": 9001},
]
@app.post("/users/", response_model=ResponseMessage, tags=["users"])
async def create_user(user: User):
return {"message": "User received"}
Einen TypeScript-Client mit Tags generieren¶
Wenn Sie unter Verwendung von Tags einen Client für eine FastAPI-Anwendung generieren, wird normalerweise auch der Client-Code anhand der Tags getrennt.
Auf diese Weise können Sie die Dinge für den Client-Code richtig ordnen und gruppieren:
In diesem Fall haben Sie:
ItemsService
UsersService
Client-Methodennamen¶
Im Moment sehen die generierten Methodennamen wie createItemItemsPost
nicht sehr sauber aus:
ItemsService.createItemItemsPost({name: "Plumbus", price: 5})
... das liegt daran, dass der Client-Generator für jede Pfadoperation die OpenAPI-interne Operation-ID verwendet.
OpenAPI erfordert, dass jede Operation-ID innerhalb aller Pfadoperationen eindeutig ist. Daher verwendet FastAPI den Funktionsnamen, den Pfad und die HTTP-Methode/-Operation, um diese Operation-ID zu generieren. Denn so kann sichergestellt werden, dass die Operation-IDs eindeutig sind.
Aber ich zeige Ihnen als nächstes, wie Sie das verbessern können. 🤓
Benutzerdefinierte Operation-IDs und bessere Methodennamen¶
Sie können die Art und Weise, wie diese Operation-IDs generiert werden, ändern, um sie einfacher zu machen und einfachere Methodennamen in den Clients zu haben.
In diesem Fall müssen Sie auf andere Weise sicherstellen, dass jede Operation-ID eindeutig ist.
Sie könnten beispielsweise sicherstellen, dass jede Pfadoperation einen Tag hat, und dann die Operation-ID basierend auf dem Tag und dem Namen der Pfadoperation (dem Funktionsnamen) generieren.
Funktion zum Generieren einer eindeutigen ID erstellen¶
FastAPI verwendet eine eindeutige ID für jede Pfadoperation, diese wird für die Operation-ID und auch für die Namen aller benötigten benutzerdefinierten Modelle für Requests oder Responses verwendet.
Sie können diese Funktion anpassen. Sie nimmt eine APIRoute
und gibt einen String zurück.
Hier verwendet sie beispielsweise den ersten Tag (Sie werden wahrscheinlich nur einen Tag haben) und den Namen der Pfadoperation (den Funktionsnamen).
Anschließend können Sie diese benutzerdefinierte Funktion als Parameter generate_unique_id_function
an FastAPI übergeben:
from fastapi import FastAPI
from fastapi.routing import APIRoute
from pydantic import BaseModel
def custom_generate_unique_id(route: APIRoute):
return f"{route.tags[0]}-{route.name}"
app = FastAPI(generate_unique_id_function=custom_generate_unique_id)
class Item(BaseModel):
name: str
price: float
class ResponseMessage(BaseModel):
message: str
class User(BaseModel):
username: str
email: str
@app.post("/items/", response_model=ResponseMessage, tags=["items"])
async def create_item(item: Item):
return {"message": "Item received"}
@app.get("/items/", response_model=list[Item], tags=["items"])
async def get_items():
return [
{"name": "Plumbus", "price": 3},
{"name": "Portal Gun", "price": 9001},
]
@app.post("/users/", response_model=ResponseMessage, tags=["users"])
async def create_user(user: User):
return {"message": "User received"}
from typing import List
from fastapi import FastAPI
from fastapi.routing import APIRoute
from pydantic import BaseModel
def custom_generate_unique_id(route: APIRoute):
return f"{route.tags[0]}-{route.name}"
app = FastAPI(generate_unique_id_function=custom_generate_unique_id)
class Item(BaseModel):
name: str
price: float
class ResponseMessage(BaseModel):
message: str
class User(BaseModel):
username: str
email: str
@app.post("/items/", response_model=ResponseMessage, tags=["items"])
async def create_item(item: Item):
return {"message": "Item received"}
@app.get("/items/", response_model=List[Item], tags=["items"])
async def get_items():
return [
{"name": "Plumbus", "price": 3},
{"name": "Portal Gun", "price": 9001},
]
@app.post("/users/", response_model=ResponseMessage, tags=["users"])
async def create_user(user: User):
return {"message": "User received"}
Einen TypeScript-Client mit benutzerdefinierten Operation-IDs generieren¶
Wenn Sie nun den Client erneut generieren, werden Sie feststellen, dass er über die verbesserten Methodennamen verfügt:
Wie Sie sehen, haben die Methodennamen jetzt den Tag und dann den Funktionsnamen, aber keine Informationen aus dem URL-Pfad und der HTTP-Operation.
Vorab-Modifikation der OpenAPI-Spezifikation für den Client-Generator¶
Der generierte Code enthält immer noch etwas verdoppelte Information.
Wir wissen bereits, dass diese Methode mit den Items zusammenhängt, da sich dieses Wort in ItemsService
befindet (vom Tag übernommen), aber wir haben auch immer noch den Tagnamen im Methodennamen vorangestellt. 😕
Wir werden das wahrscheinlich weiterhin für OpenAPI im Allgemeinen beibehalten wollen, da dadurch sichergestellt wird, dass die Operation-IDs eindeutig sind.
Aber für den generierten Client könnten wir die OpenAPI-Operation-IDs direkt vor der Generierung der Clients modifizieren, um diese Methodennamen schöner und sauberer zu machen.
Wir könnten das OpenAPI-JSON in eine Datei openapi.json
herunterladen und dann mit einem Skript wie dem folgenden den vorangestellten Tag entfernen:
import json
from pathlib import Path
file_path = Path("./openapi.json")
openapi_content = json.loads(file_path.read_text())
for path_data in openapi_content["paths"].values():
for operation in path_data.values():
tag = operation["tags"][0]
operation_id = operation["operationId"]
to_remove = f"{tag}-"
new_operation_id = operation_id[len(to_remove) :]
operation["operationId"] = new_operation_id
file_path.write_text(json.dumps(openapi_content))
import * as fs from 'fs'
async function modifyOpenAPIFile(filePath) {
try {
const data = await fs.promises.readFile(filePath)
const openapiContent = JSON.parse(data)
const paths = openapiContent.paths
for (const pathKey of Object.keys(paths)) {
const pathData = paths[pathKey]
for (const method of Object.keys(pathData)) {
const operation = pathData[method]
if (operation.tags && operation.tags.length > 0) {
const tag = operation.tags[0]
const operationId = operation.operationId
const toRemove = `${tag}-`
if (operationId.startsWith(toRemove)) {
const newOperationId = operationId.substring(toRemove.length)
operation.operationId = newOperationId
}
}
}
}
await fs.promises.writeFile(
filePath,
JSON.stringify(openapiContent, null, 2),
)
console.log('File successfully modified')
} catch (err) {
console.error('Error:', err)
}
}
const filePath = './openapi.json'
modifyOpenAPIFile(filePath)
Damit würden die Operation-IDs von Dingen wie items-get_items
in get_items
umbenannt, sodass der Client-Generator einfachere Methodennamen generieren kann.
Einen TypeScript-Client mit der modifizierten OpenAPI generieren¶
Da das Endergebnis nun in einer Datei openapi.json
vorliegt, würden Sie die package.json
ändern, um diese lokale Datei zu verwenden, zum Beispiel:
{
"name": "frontend-app",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"generate-client": "openapi --input ./openapi.json --output ./src/client --client axios --useOptions --useUnionTypes"
},
"author": "",
"license": "",
"devDependencies": {
"openapi-typescript-codegen": "^0.20.1",
"typescript": "^4.6.2"
}
}
Nach der Generierung des neuen Clients hätten Sie nun saubere Methodennamen mit allen Autovervollständigungen, Inline-Fehlerberichten, usw.:
Vorteile¶
Wenn Sie die automatisch generierten Clients verwenden, erhalten Sie automatische Codevervollständigung für:
- Methoden.
- Request-Payloads im Body, Query-Parameter, usw.
- Response-Payloads.
Außerdem erhalten Sie für alles Inline-Fehlerberichte.
Und wann immer Sie den Backend-Code aktualisieren und das Frontend neu generieren, stehen alle neuen Pfadoperationen als Methoden zur Verfügung, die alten werden entfernt und alle anderen Änderungen werden im generierten Code reflektiert. 🤓
Das bedeutet auch, dass, wenn sich etwas ändert, dies automatisch im Client-Code reflektiert wird. Und wenn Sie den Client erstellen, kommt es zu einer Fehlermeldung, wenn die verwendeten Daten nicht übereinstimmen.
Sie würden also sehr früh im Entwicklungszyklus viele Fehler erkennen, anstatt darauf warten zu müssen, dass die Fehler Ihren Endbenutzern in der Produktion angezeigt werden, und dann zu versuchen, zu debuggen, wo das Problem liegt. ✨