Ir para o conteúdo

Generate Clients

Como o FastAPI é baseado na especificação OpenAPI, você obtém compatibilidade automática com muitas ferramentas, incluindo a documentação automática da API (fornecida pelo Swagger UI).

Uma vantagem particular que nem sempre é óbvia é que você pode gerar clientes (às vezes chamados de SDKs) para a sua API, para muitas linguagens de programação diferentes.

Geradores de Clientes OpenAPI

Existem muitas ferramentas para gerar clientes a partir do OpenAPI.

Uma ferramenta comum é o OpenAPI Generator.

Se voce está construindo um frontend, uma alternativa muito interessante é o openapi-ts.

Geradores de Clientes e SDKs - Patrocinadores

Existem também alguns geradores de clientes e SDKs baseados na OpenAPI (FastAPI) patrocinados por empresas, em alguns casos eles podem oferecer recursos adicionais além de SDKs/clientes gerados de alta qualidade.

Alguns deles também ✨ patrocinam o FastAPI ✨, isso garante o desenvolvimento contínuo e saudável do FastAPI e seu ecossistema.

E isso mostra o verdadeiro compromisso deles com o FastAPI e sua comunidade (você), pois eles não apenas querem fornecer um bom serviço, mas também querem garantir que você tenha um framework bom e saudável, o FastAPI. 🙇

Por exemplo, você pode querer experimentar:

Existem também várias outras empresas que oferecem serviços semelhantes que você pode pesquisar e encontrar online. 🤓

Gerar um Cliente Frontend TypeScript

Vamos começar com um aplicativo FastAPI simples:

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},
    ]
🤓 Other versions and variants
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},
    ]

Note que as operações de rota definem os modelos que usam para o corpo da requisição e o corpo da resposta, usando os modelos Item e ResponseMessage.

Documentação da API

Se você acessar a documentação da API, verá que ela tem os schemas para os dados a serem enviados nas requisições e recebidos nas respostas:

Você pode ver esses schemas porque eles foram declarados com os modelos no app.

Essas informações estão disponíveis no OpenAPI schema do app e são mostradas na documentação da API (pelo Swagger UI).

E essas mesmas informações dos modelos que estão incluídas no OpenAPI são o que pode ser usado para gerar o código do cliente.

Gerar um Cliente TypeScript

Agora que temos o app com os modelos, podemos gerar o código do cliente para o frontend.

Instalar o openapi-ts

Você pode instalar o openapi-ts no seu código frontend com:

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

---> 100%

Gerar o Código do Cliente

Para gerar o código do cliente, você pode usar a aplicação de linha de comando openapi-ts que agora está instalada.

Como ela está instalada no projeto local, você provavelmente não conseguiria chamar esse comando diretamente, mas você o colocaria no seu arquivo package.json.

Poderia ser assim:

{
  "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"
  }
}

Depois de ter esse script NPM generate-client lá, você pode executá-lo com:

$ 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

Esse comando gerará o código em ./src/client e usará o axios (a biblioteca HTTP frontend) internamente.

Experimente o Código do Cliente

Agora você pode importar e usar o código do cliente, ele poderia ser assim, observe que você obtém preenchimento automático para os métodos:

Você também obterá preenchimento automático para o corpo a ser enviado:

Dica

Observe o preenchimento automático para name e price, que foi definido no aplicativo FastAPI, no modelo Item.

Você terá erros em linha para os dados que você envia:

O objeto de resposta também terá preenchimento automático:

App FastAPI com Tags

Em muitos casos seu app FastAPI será maior, e você provavelmente usará tags para separar diferentes grupos de operações de rota.

Por exemplo, você poderia ter uma seção para items e outra seção para users, e elas poderiam ser separadas por tags:

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"}
🤓 Other versions and variants
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"}

Gerar um Cliente TypeScript com Tags

Se você gerar um cliente para um app FastAPI usando tags, normalmente também separará o código do cliente com base nas tags.

Dessa forma, você poderá ter as coisas ordenadas e agrupadas corretamente para o código do cliente:

Nesse caso você tem:

  • ItemsService
  • UsersService

Nomes dos Métodos do Cliente

Agora os nomes dos métodos gerados como createItemItemsPost não parecem muito "limpos":

ItemsService.createItemItemsPost({name: "Plumbus", price: 5})

...isto ocorre porque o gerador de clientes usa o operation ID interno do OpenAPI para cada operação de rota.

O OpenAPI exige que cada operation ID seja único em todas as operações de rota, então o FastAPI usa o nome da função, o caminho e o método/operacao HTTP para gerar esse operation ID, porque dessa forma ele pode garantir que os operation IDs sejam únicos.

Mas eu vou te mostrar como melhorar isso a seguir. 🤓

IDs de Operação Personalizados e Melhores Nomes de Método

Você pode modificar a maneira como esses IDs de operação são gerados para torná-los mais simples e ter nomes de método mais simples nos clientes.

Neste caso, você terá que garantir que cada ID de operação seja único de alguma outra maneira.

Por exemplo, você poderia garantir que cada operação de rota tenha uma tag, e então gerar o ID da operação com base na tag e no nome da operação de rota (o nome da função).

Função Personalizada para Gerar IDs de Operação Únicos

O FastAPI usa um ID único para cada operação de rota, ele é usado para o ID da operação e também para os nomes de quaisquer modelos personalizados necessários, para requisições ou respostas.

Você pode personalizar essa função. Ela recebe uma APIRoute e gera uma string.

Por exemplo, aqui está usando a primeira tag (você provavelmente terá apenas uma tag) e o nome da operação de rota (o nome da função).

Você pode então passar essa função personalizada para o FastAPI como o parâmetro generate_unique_id_function:

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"}
🤓 Other versions and variants
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"}

Gerar um Cliente TypeScript com IDs de Operação Personalizados

Agora, se você gerar o cliente novamente, verá que ele tem os nomes dos métodos melhorados:

Como você pode ver, os nomes dos métodos agora têm a tag e, em seguida, o nome da função. Agora eles não incluem informações do caminho da URL e da operação HTTP.

Pré-processar a Especificação OpenAPI para o Gerador de Clientes

O código gerado ainda tem algumas informações duplicadas.

Nós já sabemos que esse método está relacionado aos items porque essa palavra está no ItemsService (retirada da tag), mas ainda temos o nome da tag prefixado no nome do método também. 😕

Provavelmente ainda queremos mantê-lo para o OpenAPI em geral, pois isso garantirá que os IDs de operação sejam únicos.

Mas para o cliente gerado, poderíamos modificar os IDs de operação do OpenAPI logo antes de gerar os clientes, apenas para tornar esses nomes de método mais simples.

Poderíamos baixar o JSON do OpenAPI para um arquivo openapi.json e então poderíamos remover essa tag prefixada com um script como este:

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)

Com isso, os IDs de operação seriam renomeados de coisas como items-get_items para apenas get_items, dessa forma o gerador de clientes pode gerar nomes de métodos mais simples.

Gerar um Cliente TypeScript com o OpenAPI Pré-processado

Agora, como o resultado final está em um arquivo openapi.json, você modificaria o package.json para usar esse arquivo local, por exemplo:

{
  "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"
  }
}

Depois de gerar o novo cliente, você teria agora nomes de métodos "limpos", com todo o preenchimento automático, erros em linha, etc:

Benefícios

Ao usar os clientes gerados automaticamente, você teria preenchimento automático para:

  • Métodos.
  • Corpo de requisições, parâmetros da query, etc.
  • Corpo de respostas.

Você também teria erros em linha para tudo.

E sempre que você atualizar o código do backend, e regenerar o frontend, ele teria quaisquer novas operações de rota disponíveis como métodos, as antigas removidas, e qualquer outra alteração seria refletida no código gerado. 🤓

Isso também significa que se algo mudar, será refletido no código do cliente automaticamente. E se você construir o cliente, ele dará erro se houver alguma incompatibilidade nos dados usados.

Então, você detectaria vários erros muito cedo no ciclo de desenvolvimento, em vez de ter que esperar que os erros apareçam para seus usuários finais em produção e então tentar depurar onde está o problema. ✨