O jeito certo de integrar o FastAPI com a OpenAI


Introdução
O FastAPI é uma ferramenta moderna e super eficiente, com uma comunidade que não para de crescer, conquistou seu espaço, principalmente em projetos que envolvem IA. A performance e a facilidade de uso são incríveis!
Mas, como nem tudo são flores, esse sucesso todo traz alguns desafios. Com muitas empresas começando a usar, algumas implementações podem acabar tropeçando em armadilhas comuns, simplesmente pela falta de experiência dos desenvolvedores com a ferramenta ou por seguirmos o caminho que parece mais fácil de início.
Esse post nasceu justamente de uma situação real que enfrentei num projeto e de uma thread bem interessante que acompanhei lá no GitHub do FastAPI. Vamos falar sobre como gerenciar o cliente da OpenAI (ou qualquer outro recurso similar) do jeito certo dentro do FastAPI usando o lifespan.
O cenário
Imagine que estamos construindo o backend para um chat, tipo um ChatGPT mais simples. Poderia ser para uma interface customizada, para consumo sob demanda, ou até para usar modelos open-source compatíveis com a biblioteca openai do Python.
E para essa missão, claro, vamos usar o FastAPI, já que até as SPAs eu estou substituindo pelo FastAPI.
Pra você que está lendo, não ficar perdido. Eu vou mostrar primeiro a forma como muita gente começa: o jeito simples, que funciona na hora, mas que esconde alguns riscos e pode trazer muita dor de cabeça lá na frente.
Depois, vamos mergulhar na solução usando o lifespan do FastAPI. O foco desse artigo é mostrar como usar o lifespan especificamente para integrar o cliente da biblioteca openai de forma correta. O lifespan tem mais utilidades, mas não vamos nos aprofundar em todas elas hoje, talvez em um post futuro!
O jeito ruim de fazer: Instânciando o cliente no topo do arquivo
Quando estamos começando um projeto, a intuição (e muitos exemplos por aí) nos leva a fazer algo assim: instanciar o cliente assíncrono da openai diretamente no escopo global do nosso arquivo principal. É simples, né?
# main.py
from fastapi import FastAPI
from openai import AsyncOpenAI
import os
client = AsyncOpenAI(api_key=os.getenv("OPENAI_API_KEY"))
app = FastAPI()
@app.post("/chat")
async def chat(prompt: str):
# Usando o cliente global diretamente
response = await client.chat.completions.create(
model="..."
messages=[{"role": "user", "content": prompt}]
)
return {"reply": response.choices[0].message.content}
Bom, como eu disse, isso funciona. Você roda o Uvicorn e consegue fazer chamadas. Mas por baixo dos panos, vários problemas estão se acumulando:
Instanciação prematura: O cliente é criado antes mesmo do servidor FastAPI estar totalmente pronto para receber requisições. Se você precisasse carregar alguma configuração assíncrona antes de criar o cliente, já não daria certo.
Gerenciamento de recursos falhos: Essa instância cliente fica "viva" durante todo o tempo que o processo do servidor rodar. Mais importante: ela não é fechada corretamente quando o servidor termina. O AsyncOpenAI usa o httpx.AsyncClient por baixo, que mantém um pool de conexões HTTP. Sem chamar
await client.aclose()
, essas conexões ficam abertas, vazando recursos (sockets, memória).Dificuldade em testes: Testar endpoints que dependem de estado global é um pé no saco. Mockar ou substituir essa instância cliente para testes unitários ou de integração fica bem mais complicado.
Replicação por worker: Se você rodar sua aplicação com múltiplos workers (como o Gunicorn faz em produção), cada worker vai criar sua própria instância global do cliente, multiplicando o desperdício de recursos e os problemas de conexão.
“O jeito certo”: Usando o lifespan do FastAPI
Eu coloquei entre aspas pois existem outras formas de fazer, como o before ou on_event e outras formas que também pode funcionar e ao mesmo tempo seguir as boas práticas. Cagar regra na nossa área definitivamente não é uma boa prática.
Voltando ao assunto. Felizmente, o FastAPI oferece uma solução elegante para gerenciar o ciclo de vida de recursos como nosso cliente openai: o parâmetro lifespan.
Ele utiliza um async context manager (gerenciador de contexto assíncrono) para garantir que seu recurso seja inicializado depois que a aplicação começa e finalizado antes que ela termine completamente.
Veja como fica bem mais interessante:
# main.py
import os
from openai import AsyncOpenAI
from fastapi import FastAPI, Request
from contextlib import asynccontextmanager
# O gerenciador de contexto para o ciclo de vida
@asynccontextmanager
async def lifespan(app: FastAPI):
# Setup: Roda antes da aplicação iniciar
print("Iniciando cliente OpenAI...")
app.state.openai_client = AsyncOpenAI(api_key=os.getenv("OPENAI_API_KEY"))
print("Cliente OpenAI pronto!")
yield # Essa função congela nesse momento e aplicação entra em ação
# Teardown: A mágica acontece aqui, depois que voc6e para a aplicação esse código é executado
print("Fechando cliente OpenAI...")
await app.state.openai_client.close()
print("Cliente OpenAI fechado.")
app = FastAPI(lifespan=lifespan)
@app.post("/chat")
async def chat(request: Request, prompt: str): # Simplificado para receber só o prompt
# Acessa o cliente via app.state
client: AsyncOpenAI = request.app.state.openai_client
response = await client.chat.completions.create(c
model="...",
messages=[{"role": "user", "content": prompt}]
)
return {"reply": response.choices[0].message.content}
Entendendo a mágica: asynccontextmanager e yield
O decorador @asynccontextmanager
(da biblioteca contextlib do Python) é a chave aqui. Ele transforma nossa função lifespan
em um gerenciador de contexto assíncrono especial.
Antes do yield: Todo o código antes da linha yield é a fase de setup. O FastAPI executa isso durante a inicialização (aqui eu imagino que você já entendeu o lance). É aqui que criamos nosso cliente AsyncOpenAI e o guardamos em
app.state.openai_client
. Oapp.state
é um objeto tipo dicionário feito exatamente para guardar recursos que precisam viver junto com a aplicação.O yield: A palavra yield é o ponto de "pausa". A função
lifespan
fica congelada aqui, e o controle volta para o FastAPI, que finalmente começa a aceitar e processar requisições. Enquanto a aplicação está rodando, nossos endpoints (como/chat
) podem acessar o cliente viarequest.app
.state.openai_client
.Depois do yield: Quando o FastAPI recebe um sinal para desligar (por exemplo, quando você pressiona Ctrl+C no terminal), ele "descongela" a função
lifespan
depois doyield
. Essa é a fase de teardown. Aqui, executamos a limpeza:await client.close()
, garantindo que as conexões HTTP sejam fechadas corretamente.
Conclusão
Sim, instanciar o cliente globalmente vai funcionar no começo. O FastAPI é robusto e vai saber lidar com isso. Mas essa abordagem não escala bem e esconde problemas que vão te assombrar em produção ou em aplicações maiores.
E pra ser honesto, eu usei a openai para chamar a sua atenção mas a minha verdadeira intenção era te mostrar o uso do lifespan.
Subscribe to my newsletter
Read articles from Ricardo Santos directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Ricardo Santos
Ricardo Santos
Sou um desenvolvedor de software movido pela curiosidade e pela paixão por tecnologia. Aqui no blog, compartilho minhas aventuras desbravando novas ferramentas, frameworks e ideias, sempre testando o que há de mais interessante no mundo digital.