Relevant for AI Apps: How Your Frontend Talks to a FastAPI Backend

~13 min read

Tying the whole unit together with the concrete shape of a real AI app: a browser frontend, a FastAPI backend, and the LLM provider as a THIRD party the backend talks to on the frontend's behalf.

Every idea from this unit — client/server roles, the HTTP request lifecycle, sync vs async — comes together in the concrete architecture of a real AI application, which usually involves not two but THREE parties.

The frontend is the client from the client-server-model subtopic: code running in the USER's browser (commonly built with a framework like Next.js/React), responsible for displaying the chat interface, capturing what the user types, and rendering the AI's response as it comes back. The backend (commonly a Python framework like FastAPI) is YOUR server — it receives requests from the frontend, and critically, it does NOT usually run the LLM itself. Instead, the backend acts as a client ITSELF toward a third party: the LLM provider's API (OpenAI, Anthropic, etc.), exactly as covered in the api-basics unit.

So a typical request actually makes TWO hops, not one: (1) the frontend sends a request to YOUR backend ('here's what the user typed'), and (2) YOUR backend, acting as a client now, sends its OWN request to the LLM provider's API ('generate a response to this'), waits for (or streams) the result, and relays it back to the frontend as the response to hop #1. Your backend is simultaneously a SERVER (from the frontend's perspective) and a CLIENT (from the LLM provider's perspective) — the same machine plays both roles, just toward different parties.

Why have your OWN backend in the middle at all, rather than having the frontend call the LLM provider directly? Mainly for the authentication reasons from the api-basics unit: your LLM provider's API key must never live in frontend JavaScript (any user could open their browser's developer tools and steal it, then run up charges on your account). Your backend keeps that secret safely on the server side, authenticates to the LLM provider itself, and only exposes to the frontend whatever narrower, safer interface YOUR application actually needs (e.g. 'send a chat message,' not 'here's my raw OpenAI key, do whatever you want'). Your backend can also apply its own rate limiting, logging, and business logic (like the rate limiter and progress-tracking modules in a real app's backend) in that middle hop — none of which the frontend should be trusted to enforce on its own, since any client-side check can be bypassed by a user who controls their own browser.

💻 Code example

# The shape of the two-hop flow: frontend -> your backend -> LLM provider,
# with your backend playing BOTH server (to the frontend) and client
# (to the provider) roles. Simulated locally, no real network calls.

import os

def llm_provider_api(prompt: str, api_key: str) -> str:
    """Stand-in for a real call to OpenAI/Anthropic -- the backend is
    the CLIENT here. The api_key never leaves the backend."""
    if not api_key:
        raise PermissionError("missing provider API key")
    return f"[LLM-generated reply to: {prompt!r}]"

class YourBackend:
    """Plays TWO roles: a SERVER to the frontend, a CLIENT to the LLM provider."""
    def __init__(self):
        self._provider_api_key = os.environ.get("OPENAI_API_KEY", "sk-demo-key")
        self._requests_this_minute = 0

    def handle_chat_request(self, user_message: str) -> dict:
        """Hop #1: receives a request FROM the frontend (server role)."""
        self._requests_this_minute += 1
        if self._requests_this_minute > 100:                 # rate limiting the frontend can't be trusted to self-enforce
            return {"status": 429, "error": "rate limited"}

        # Hop #2: the backend now acts as a CLIENT toward the LLM provider
        try:
            reply = llm_provider_api(user_message, self._provider_api_key)
        except PermissionError as e:
            return {"status": 500, "error": str(e)}

        return {"status": 200, "reply": reply}  # relayed back as the response to hop #1

backend = YourBackend()
print(backend.handle_chat_request("What is a client-server model?"))

💬 Deep Dive with AI

Key points

  • A real AI app usually has three parties: the frontend (browser client), your backend (e.g. FastAPI), and the LLM provider's API — not just two
  • A request makes two hops: frontend -> your backend, then your backend -> the LLM provider — your backend is a server to one side and a client to the other
  • Keeping the LLM provider's API key on the backend (never in frontend JavaScript) prevents any user from stealing it via their browser's developer tools
  • Your backend can enforce rate limiting, logging, and business logic in that middle hop — checks a client-side-only app could never reliably enforce
  • This two-hop, dual-role pattern is the standard shape behind essentially every production AI chat application