Python remains the default language for data teams, ML engineers, and many backend developers. If you are adding chat or completion features to a Python service, FastAPI is a strong choice: async-friendly, typed, and easy to deploy behind Gunicorn or Uvicorn. This tutorial shows how to call a REST AI API from FastAPI without exposing your key to clients.
If you already use Next.js on the frontend, pair this guide with our Next.js AI API integration so the browser talks only to your own backend.
Prerequisites
- Python 3.11+
- A Daymora API key (stored as an environment variable)
- Basic familiarity with HTTP and JSON
Project setup
Create a virtual environment and install dependencies:
python -m venv .venv
source .venv/bin/activate
pip install fastapi uvicorn httpx python-dotenvAdd .env (never commit it):
DAYMORA_API_KEY=your_key_hereFollow the same key hygiene as in How to manage AI API keys securely.
Non-streaming chat endpoint
Create main.py:
import os
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import httpx
app = FastAPI()
API_URL = "https://daymora.com/api/v1/chat"
class Message(BaseModel):
role: str
content: str
class ChatRequest(BaseModel):
messages: list[Message]
model: str = "gpt-5"
@app.post("/api/chat")
async def chat(body: ChatRequest):
api_key = os.environ.get("DAYMORA_API_KEY")
if not api_key:
raise HTTPException(status_code=500, detail="API key not configured")
async with httpx.AsyncClient(timeout=120.0) as client:
response = await client.post(
API_URL,
headers={
"API_SECRET": api_key,
"Content-Type": "application/json",
},
json={
"model": body.model,
"messages": [m.model_dump() for m in body.messages],
"stream": False,
},
)
if response.status_code >= 400:
raise HTTPException(status_code=response.status_code, detail=response.text)
return response.json()Run locally:
uvicorn main:app --reload --port 8000Streaming with Server-Sent Events
Most chat UIs expect tokens as they arrive. Enable streaming on the upstream request and forward the byte stream:
from fastapi.responses import StreamingResponse
@app.post("/api/chat/stream")
async def chat_stream(body: ChatRequest):
api_key = os.environ["DAYMORA_API_KEY"]
async def event_generator():
async with httpx.AsyncClient(timeout=None) as client:
async with client.stream(
"POST",
API_URL,
headers={"API_SECRET": api_key, "Content-Type": "application/json"},
json={
"model": body.model,
"messages": [m.model_dump() for m in body.messages],
"stream": True,
},
) as response:
if response.status_code >= 400:
yield f"data: {{\"error\": \"upstream failed\"}}\n\n"
return
async for chunk in response.aiter_bytes():
yield chunk
return StreamingResponse(event_generator(), media_type="text/event-stream")For a deeper look at why SSE is the common default for LLM APIs, see AI API streaming: SSE vs WebSockets.
Rate limiting and abuse protection
Expose these routes only to authenticated users in your product. Add per-user limits so one account cannot burn through your quota — patterns mirror the Next.js examples in our API key security guide.
Cost predictability
Python services often fan out many internal jobs (summaries, classifiers, embeddings pipelines). Token billing makes each cron run a line item on your invoice. If AI is on the critical path every day, compare your measured monthly usage against flat-rate options in What is a flat-rate AI API?.
Deploying to production
- Inject
DAYMORA_API_KEYvia your host's secret store (Railway, Fly.io, AWS, etc.) - Set
timeoutgenerously for long completions but cap max tokens in the JSON body - Log latency and status codes, not full prompts, unless you have a retention policy
Summary
A FastAPI proxy keeps credentials server-side, gives you one place for auth and rate limits, and works with both JSON and streaming clients. Start non-streaming to validate prompts, then add SSE for the chat UX your users expect.