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
lifespanfica 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
lifespandepois 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.



