Genera Clientes¶
Como FastAPI está basado en la especificación OpenAPI, obtienes compatibilidad automática con muchas herramientas, incluyendo la documentación automática de la API (proporcionada por Swagger UI).
Una ventaja particular que no es necesariamente obvia es que puedes generar clientes (a veces llamados SDKs ) para tu API, para muchos lenguajes de programación diferentes.
Generadores de Clientes OpenAPI¶
Hay muchas herramientas para generar clientes desde OpenAPI.
Una herramienta común es OpenAPI Generator.
Si estás construyendo un frontend, una alternativa muy interesante es openapi-ts.
Generadores de Clientes y SDKs - Sponsor¶
También hay algunos generadores de Clientes y SDKs respaldados por empresas basados en OpenAPI (FastAPI), en algunos casos pueden ofrecerte funcionalidades adicionales además de SDKs/clientes generados de alta calidad.
Algunos de ellos también ✨ sponsorean FastAPI ✨, esto asegura el desarrollo continuo y saludable de FastAPI y su ecosistema.
Y muestra su verdadero compromiso con FastAPI y su comunidad (tú), ya que no solo quieren proporcionarte un buen servicio sino también asegurarse de que tengas un buen y saludable framework, FastAPI. 🙇
Por ejemplo, podrías querer probar:
También hay varias otras empresas que ofrecen servicios similares que puedes buscar y encontrar en línea. 🤓
Genera un Cliente Frontend en 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},
]
🤓 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},
]
Nota que las path operations definen los modelos que usan para el payload de la petición y el payload del response, usando los modelos Item
y ResponseMessage
.
Documentación de la API¶
Si vas a la documentación de la API, verás que tiene los esquemas para los datos que se enviarán en las peticiones y se recibirán en los responses:
Puedes ver esos esquemas porque fueron declarados con los modelos en la aplicación.
Esa información está disponible en el JSON Schema de OpenAPI de la aplicación, y luego se muestra en la documentación de la API (por Swagger UI).
Y esa misma información de los modelos que está incluida en OpenAPI es lo que puede usarse para generar el código del cliente.
Genera un Cliente en TypeScript¶
Ahora que tenemos la aplicación con los modelos, podemos generar el código del cliente para el frontend.
Instalar openapi-ts
¶
Puedes instalar openapi-ts
en tu código de frontend con:
$ npm install @hey-api/openapi-ts --save-dev
---> 100%
Generar el Código del Cliente¶
Para generar el código del cliente puedes usar la aplicación de línea de comandos openapi-ts
que ahora estaría instalada.
Como está instalada en el proyecto local, probablemente no podrías llamar a ese comando directamente, pero podrías ponerlo en tu archivo package.json
.
Podría verse como esto:
{
"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"
}
}
Después de tener ese script de NPM generate-client
allí, puedes ejecutarlo con:
$ 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
Ese comando generará código en ./src/client
y usará axios
(el paquete HTTP de frontend) internamente.
Prueba el Código del Cliente¶
Ahora puedes importar y usar el código del cliente, podría verse así, nota que tienes autocompletado para los métodos:
También obtendrás autocompletado para el payload a enviar:
Consejo
Nota el autocompletado para name
y price
, que fue definido en la aplicación de FastAPI, en el modelo Item
.
Tendrás errores en línea para los datos que envíes:
El objeto de response también tendrá autocompletado:
App de FastAPI con Tags¶
En muchos casos tu aplicación 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 usuarios, y podrían estar 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"}
Genera un Cliente TypeScript con Tags¶
Si generas un cliente para una aplicación 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 los Métodos del Cliente¶
Ahora mismo los nombres de los 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 a través de todas las path operations, por lo 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 asegurarse de 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 forma 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 asegurarte de que cada operation ID sea único de alguna otra manera.
Por ejemplo, podrías asegurarte de que cada path operation tenga un tag, y luego generar el operation ID basado en el tag y el nombre de la path operation name (el nombre de la función).
Función Personalizada para Generar ID Único¶
FastAPI usa un ID único para cada path operation, 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 un APIRoute
y retorna un string.
Por ejemplo, aquí está usando el primer tag (probablemente tendrás solo un tag) y el nombre de la path operation (el nombre de la función).
Puedes entonces 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"}
🤓 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"}
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.
Preprocesa la Especificación OpenAPI para el Generador de Clientes¶
El código generado aún tiene algo de información duplicada.
Ya sabemos que este método está relacionado con los items porque esa palabra está en el ItemsService
(tomado del tag), pero aún tenemos el nombre del tag prefijado en el nombre del método también. 😕
Probablemente aún 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 solo 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¶
Ahora como el resultado final está en un archivo openapi.json
, modificarías el package.json
para usar ese archivo local, por ejemplo:
{
"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"
}
}
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¶
Cuando usas los clientes generados automáticamente obtendrás autocompletado para:
- Métodos.
- Payloads de peticiones en el cuerpo, parámetros de query, etc.
- Payloads de responses.
También tendrás errores en línea para todo.
Y cada vez que actualices el código del backend, y regeneres el frontend, tendrás las nuevas path operations disponibles como métodos, las antiguas eliminadas, y cualquier otro cambio se reflejará en el código generado. 🤓
Esto también significa que si algo cambió será reflejado automáticamente en el código del cliente. Y si haces build del cliente, te dará error si tienes algún desajuste en los datos utilizados.
Así que, detectarás muchos errores muy temprano en el ciclo de desarrollo en lugar de tener que esperar a que los errores se muestren a tus usuarios finales en producción para luego intentar depurar dónde está el problema. ✨