Saltar al contenido

Generando SDKs

Como FastAPI está basado en la especificación de OpenAPI, sus APIs pueden ser descritas en un formato estándar que muchas herramientas entienden.

Esto hace fácil generar documentación actualizada, librerías cliente (SDKs) en múltiples lenguajes, y testing o flujos de automatización que se mantienen sincronizados con tu código.

En esta guía, aprenderás cómo generar un SDK de TypeScript para tu backend de FastAPI.

Generadores de SDK Open Source

Una opción versátil es el OpenAPI Generator, que soporta muchos lenguajes de programación y puede generar SDKs desde tu especificación OpenAPI.

Para clientes TypeScript, Hey API es una solución construida específicamente, que proporciona una experiencia optimizada para el ecosistema de TypeScript.

Puedes descubrir más generadores de SDK en OpenAPI.Tools.

Consejo

FastAPI genera automáticamente especificaciones de OpenAPI 3.1, así que cualquier herramienta que uses debe soportar esta versión.

Crear un SDK de TypeScript

Empecemos con una aplicación simple de FastAPI:

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},
    ]

Notá que las path operations definen los modelos que usan para el payload de request y el payload de response, usando los modelos Item y ResponseMessage.

Docs de la API

Si vas a /docs, verás que tiene los schemas para los datos a enviar en las requests y recibir en las responses:

Puedes ver esos schemas porque fueron declarados con los modelos en la app.

Esa información está disponible en el schema de OpenAPI de la app, y luego se muestra en la documentación de la API.

Esa misma información de los modelos que está incluida en OpenAPI es lo que se puede usar para generar el código del cliente.

Hey API

Una vez que tenemos una app de FastAPI con los modelos, podemos usar Hey API para generar un cliente TypeScript. La forma más rápida de hacerlo es vía npx.

npx @hey-api/openapi-ts -i http://localhost:8000/openapi.json -o src/client

Esto generará un SDK de TypeScript en ./src/client.

Puedes aprender cómo instalar @hey-api/openapi-ts y leer sobre la salida generada en su sitio web.

Usando el SDK

Ahora puedes importar y usar el código del cliente. Podría verse así, notá que obtienes autocompletado para los métodos:

También obtendrás autocompletado para el payload a enviar:

Consejo

Notá el autocompletado para name y price, que fue definido en la aplicación FastAPI, en el modelo Item.

Tendrás errores en línea para los datos que envías:

El objeto de response también tendrá autocompletado:

App de FastAPI con Tags

En muchos casos, tu app de FastAPI será más grande, y probablemente usarás tags para separar diferentes grupos de path operations.

Por ejemplo, podrías tener una sección para items y otra sección para users, y podrían 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"}

Generar un Cliente TypeScript con Tags

Si generas un cliente para una app de FastAPI usando tags, normalmente también separará el código del cliente basándose en los tags.

De esta manera, podrás tener las cosas ordenadas y agrupadas correctamente para el código del cliente:

En este caso, tienes:

  • ItemsService
  • UsersService

Nombres de Métodos del Cliente

Ahora mismo, los nombres de métodos generados como createItemItemsPost no se ven muy limpios:

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

...eso es porque el generador del cliente usa el operation ID interno de OpenAPI para cada path operation.

OpenAPI requiere que cada operation ID sea único en todas las path operations, así que FastAPI usa el nombre de la función, el path, y el método/operación HTTP para generar ese operation ID, porque de esa manera puede asegurar que los operation IDs sean únicos.

Pero te mostraré cómo mejorar eso a continuación. 🤓

Operation IDs Personalizados y Mejores Nombres de Métodos

Puedes modificar la manera en que estos operation IDs son generados para hacerlos más simples y tener nombres de métodos más simples en los clientes.

En este caso, tendrás que asegurar que cada operation ID sea único de alguna otra manera.

Por ejemplo, podrías asegurar que cada path operation tenga un tag, y luego generar el operation ID basado en el tag y el nombre de la path operation (el nombre de la función).

Función Personalizada para Generar ID Único

FastAPI usa un ID único para cada path operation, que se usa para el operation ID y también para los nombres de cualquier modelo personalizado necesario, para requests o responses.

Puedes personalizar esa función. Toma una APIRoute y retorna un string.

Por ejemplo, aquí se usa el primer tag (probablemente tendrás solo un tag) y el nombre de la path operation (el nombre de la función).

Luego puedes pasar esa función personalizada a FastAPI como el 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"}

Generar un Cliente TypeScript con Operation IDs Personalizados

Ahora, si generas el cliente de nuevo, verás que tiene los nombres de métodos mejorados:

Como ves, los nombres de métodos ahora tienen el tag y luego el nombre de la función, ahora no incluyen información del path de la URL y la operación HTTP.

Preprocesar la Especificación OpenAPI para el Generador de Clientes

El código generado todavía tiene algo de información duplicada.

Ya sabemos que este método está relacionado con los items porque esa palabra está en ItemsService (tomado del tag), pero todavía tenemos el nombre del tag como prefijo en el nombre del método también. 😕

Probablemente todavía querremos mantenerlo para OpenAPI en general, ya que eso asegurará que los operation IDs sean únicos.

Pero para el cliente generado, podríamos modificar los operation IDs de OpenAPI justo antes de generar los clientes, solo para hacer esos nombres de métodos más bonitos y limpios.

Podríamos descargar el JSON de OpenAPI a un archivo openapi.json y luego podríamos remover ese tag prefijado con un 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)

Con eso, los operation IDs serían renombrados de cosas como items-get_items a simplemente get_items, de esa manera el generador del cliente puede generar nombres de métodos más simples.

Generar un Cliente TypeScript con el OpenAPI Preprocesado

Como el resultado final ahora está en un archivo openapi.json, necesitas actualizar tu ubicación de entrada:

npx @hey-api/openapi-ts -i ./openapi.json -o src/client

Después de generar el nuevo cliente, ahora tendrías nombres de métodos limpios, con todo el autocompletado, errores en línea, etc:

Beneficios

Al usar los clientes generados automáticamente, obtendrías autocompletado para:

  • Métodos.
  • Payloads de request en el body, query parameters, etc.
  • Payloads de response.

También tendrías errores en línea para todo.

Y cuando actualices el código del backend, y regeneres el frontend, tendría cualquier nueva path operation disponible como métodos, las viejas removidas, y cualquier otro cambio se reflejaría en el código generado. 🤓

Esto también significa que si algo cambió, se reflejará en el código del cliente automáticamente. Y si compilas el cliente, dará error si tienes alguna discrepancia en los datos usados.

Así que, detectarías muchos errores muy temprano en el ciclo de desarrollo en lugar de tener que esperar a que los errores le aparezcan a tus usuarios finales en producción y luego intentar debuggear dónde está el problema. ✨