Guide

Next.js and FastAPI for AI apps: structure, auth and streaming

Use Next.js for the interface and the user's session, and FastAPI for model calls, retrieval and agent logic. Let the browser talk only to Next.js where possible, have Next.js forward requests with a short-lived signed token, verify that token in FastAPI, and stream responses through as server-sent events using fetch.

Why use two frameworks at all

A second backend is not automatically justified. If your AI feature is a single call to a hosted model, a Next.js Route Handler may be all you need. A separate Python service earns its place when you depend on Python's AI ecosystem for retrieval, document processing or orchestration, when agent runs outlast a typical web request, or when one team owns the model logic and another owns the product.

The Next.js documentation is itself clear on this point. In its backend for frontend guide, it describes these capabilities as an API layer and says they are not a full backend replacement. It also warns that some hosts deploy Route Handlers as lambda functions, where long-running handlers may be terminated. That matters for AI work, where a single request can run for a long time.

A structure that keeps the boundary visible

A monorepo with two applications and a shared contract works well for a small team:

# Illustrative layout; names are examples, not a required convention.
apps/
  web/                      # Next.js
    app/
      (chat)/page.tsx       # Server and Client Components
      api/chat/route.ts     # Same-origin route to the AI service
    lib/session.ts          # Session cookie, server only
    lib/ai-client.ts        # Calls to FastAPI, server only
  ai/                       # FastAPI
    app/
      main.py
      auth.py               # Verifies tokens from web
      routes/chat.py        # Thin HTTP layer
      services/             # Prompts, retrieval, agent logic
contracts/
  openapi.json              # Generated from FastAPI

This layout is illustrative. The file worth having is the contract: FastAPI generates an OpenAPI schema, which you can use to generate TypeScript types, so that a changed response shape fails in the build and not in front of a user. Streamed events need the same discipline, so give them explicit types as well. The internal layout of the Python side is covered in serving an LLM app with FastAPI.

Deciding what goes where

ConcernNext.jsFastAPI
Pages, forms, chat interfaceYesNo
Sign-in and session cookieYesNo, it receives identity
Ordinary product dataYes, through a data access layerOnly what AI work needs
Prompts and model API keysNoYes
Retrieval, tools and agent stateNoYes
Rate limits and quotasCoarse, per userFine, per model and per run
Long-running workStarts it, shows statusRuns it, often in a worker

One rule resolves most disputes: anything that changes what the model does belongs in the AI service. Should prompts creep into TypeScript, you end up with two places to evaluate and version them.

Next.js gives you several ways to reach the backend, and they suit different jobs. The documentation advises fetching data in Server Components directly from its source instead of calling your own Route Handlers, which adds a round trip. A Server Component can therefore call FastAPI directly for conversation history. Server Actions are designed for mutations and are queued, so they suit "rename this chat" but not a live stream. Route Handlers are the natural fit for streaming chat from a Client Component.

Authentication across the boundary

Three separate questions are often merged here: who the user is, whether Next.js is really the caller, and what this user may do.

In Next.js: own the session

Whether you use a library or your own code, keep the session in a cookie set on the server. The Next.js authentication guide recommends the HttpOnly and Secure options, together with SameSite and an expiry. HttpOnly keeps the cookie out of reach of client-side JavaScript, which helps when an app renders model output that may contain surprises.

The guide's other warning is that Proxy, the file formerly known as Middleware, should not be your only line of defence, and that most checks should sit close to the data source. Route Handlers are public endpoints, so each one needs its own session check. Hiding a button is not a permission check.

Between services: a short-lived signed token

When forwarding to FastAPI, do not copy all incoming headers. The backend for frontend guide points out that doing so passes along the user's cookie and authorisation values. Instead, mint a token containing only what the service needs, such as the user ID, tenant and role, with a short expiry and a specific audience. A shared static API key would prove only that the caller is your app; it says nothing about which user a request is for.

In FastAPI: verify, then enforce

# Illustrative only. Secret handling and key choice are simplified.
from typing import Annotated

import jwt
from fastapi import Depends, Header, HTTPException

SECRET_KEY = "load-from-a-secret-store"

def current_user(authorization: Annotated[str, Header()]) -> dict:
    scheme, _, token = authorization.partition(" ")
    if scheme.lower() != "bearer" or not token:
        raise HTTPException(status_code=401)
    try:
        return jwt.decode(
            token, SECRET_KEY, algorithms=["HS256"], audience="ai-service"
        )
    except jwt.InvalidTokenError:
        raise HTTPException(status_code=401)

CurrentUser = Annotated[dict, Depends(current_user)]

This sample is illustrative and uses PyJWT, the library shown in FastAPI's security tutorial. A shared secret is the simplest option; asymmetric keys mean the AI service can verify tokens without being able to create them. Take the user ID from the verified token and never from the request body or from model output, then apply it when filtering retrieval and running tools. Network restrictions that make the service private are a welcome extra layer, but keep the token check too.

Do you need CORS?

If the browser calls only Next.js, FastAPI receives server-to-server requests and CORS is not involved. If the browser calls FastAPI directly, perhaps to avoid a platform timeout, you do need it. FastAPI's CORS documentation notes that wildcard origins exclude credentialed communication such as cookies and Authorization headers, so list your origins explicitly.

Streaming through the stack

On the FastAPI side, recent versions have built-in server-sent events. The SSE tutorial says this support was added in version 0.135.0, works with POST, sends a keep-alive comment when a connection has been idle for 15 seconds, and sets headers to discourage caching and proxy buffering.

# Illustrative only. stream_answer is a placeholder async generator.
from collections.abc import AsyncIterable

from fastapi import FastAPI
from fastapi.sse import EventSourceResponse, ServerSentEvent
from pydantic import BaseModel

from .auth import CurrentUser
from .services import stream_answer

app = FastAPI()

class ChatRequest(BaseModel):
    conversation_id: str
    message: str

@app.post("/chat", response_class=EventSourceResponse)
async def chat(body: ChatRequest, user: CurrentUser) -> AsyncIterable[ServerSentEvent]:
    async for token in stream_answer(body, user["sub"]):
        yield ServerSentEvent(data={"text": token}, event="token")
    yield ServerSentEvent(data={"finished": True}, event="done")

Between the two services you have two main options:

  • A Route Handler that passes the stream on. It can check the session, mint a token, call FastAPI and return the upstream body as the response without reading it fully. This suits most apps, because your auth check runs on every request. The upstream connection does count against your host's limits on function duration.
  • A rewrite. The documentation recommends rewrites for large payloads or streaming responses, as they forward without your handler holding the body. A rewrite cannot run per-request logic by itself, so FastAPI must be able to authenticate the request some other way.

A pass-through handler can stay short:

// Illustrative only: app/api/chat/route.ts. verifySession and mintToken
// are placeholders for your own session helpers.
export async function POST(request: Request) {
  const session = await verifySession()
  if (!session) return new Response(null, { status: 401 })

  const upstream = await fetch(`${process.env.AI_SERVICE_URL}/chat`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${await mintToken(session)}`,
    },
    body: await request.text(),
    signal: request.signal,
  })

  if (!upstream.ok || !upstream.body) {
    return Response.json({ error: 'AI service error' }, { status: 502 })
  }
  return new Response(upstream.body, {
    headers: { 'Content-Type': 'text/event-stream' },
  })
}

This sample is illustrative and should have its request body validated before use. Passing request.signal aims to end the upstream call when the user stops or leaves, but confirm that cancellation reaches the model on your host, or you may keep paying for tokens nobody will read.

In the browser, read the response with fetch. The EventSource interface, as documented, takes a URL and a credentials option but offers no way to send a POST body or custom headers. The details of parsing events, handling interruptions and rendering partial text are in streaming LLM responses to a web UI, and the interface states are in React frontend for LLM chat apps.

When a stream is the wrong shape

A request held open works for a chat reply. A multi-minute agent run should not depend on one connection surviving. Move it into a stored run that a worker executes, with the browser following its progress and able to reconnect, as described in building a backend API for an AI agent. Next.js then only starts the run and displays its state.

If you are planning a product along these lines, the full stack development page describes what I can help with.

Frequently asked questions

Do I need FastAPI if I already use Next.js?

Not always. For a simple call to a hosted model, a Route Handler can be enough. A Python service makes more sense when you rely on Python AI libraries, run long agent work or want model logic owned and deployed separately.

Should the browser call FastAPI directly?

Usually it is simpler for the browser to call Next.js, which keeps sessions and service credentials on the server and avoids CORS. A direct connection can make sense if a host's limits interfere with streaming, but it then needs its own authentication and explicit allowed origins.

How should FastAPI know which user is making a request?

Have Next.js verify the session, then send a short-lived signed token containing the user's identity. FastAPI verifies the signature, expiry and audience, then uses that identity for filtering and tool permissions. Never accept a user ID from the request body.

Can I use EventSource to stream chat responses?

EventSource as documented has no way to send a POST body or custom headers, which chat requests usually need. Use fetch and read the response body as a stream, handling the parsing and reconnection yourself.