Zum Inhalt

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-ts 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-ts installieren

Sie können openapi-ts in Ihrem Frontend-Code installieren mit:

$ npm install @hey-api/openapi-ts --save-dev

---> 100%

Client-Code generieren

Um den Client-Code zu generieren, können Sie das Kommandozeilentool openapi-ts 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-ts --input http://localhost:8000/openapi.json --output ./src/client --client axios"
  },
  "author": "",
  "license": "",
  "devDependencies": {
    "@hey-api/openapi-ts": "^0.27.38",
    "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-ts --input http://localhost:8000/openapi.json --output ./src/client --client axios

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-ts --input ./openapi.json --output ./src/client --client axios"
  },
  "author": "",
  "license": "",
  "devDependencies": {
    "@hey-api/openapi-ts": "^0.27.38",
    "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. ✨