# Rabinarayan Patra โ€” Full Content Archive > This is the full-content companion to /llms.txt. It inlines the raw > markdown of every published post so AI systems can cite passages > without a second fetch. Generated from the live blog index. > Author: Rabinarayan Patra (SDE II at Amazon) > Website: https://www.rabinarayanpatra.com > Licensing: content may be quoted with attribution and a link back to the source URL. --- ## How to Serve a React SPA from FastAPI with app.frontend() Source: https://www.rabinarayanpatra.com/blogs/serve-react-spa-fastapi-app-frontend Published: 2026-10-23T00:00:00+05:30 Tags: serve-react-frontend-fastapi, fastapi, app-frontend, react, vite, full-stack, python FastAPI 0.138.0 adds app.frontend() to serve a React or Vite SPA in one line, no StaticFiles mount. Here is how it routes, falls back, and where it stops short. Every time I shipped a React app behind FastAPI, I lost twenty minutes to the same plumbing. Mount `StaticFiles`, add a catch-all route so a page refresh on `/dashboard` doesn't 404, then fight the ordering so the catch-all doesn't swallow my `/api` endpoints. It worked. But it was copy-paste glue I never enjoyed writing. FastAPI 0.138.0, released June 20, 2026, makes that glue obsolete. There's a new method, `app.frontend()`, that serves a built single-page app in one line and gets the routing right by default. Here's how to serve a React or Vite SPA with it, how it decides between your API and your frontend, and the one thing it deliberately won't do. ## What is app.frontend() in FastAPI? `app.frontend()` is a method added in FastAPI 0.138.0 that serves a pre-built static frontend, like a compiled React, Vue, or Svelte app, directly from your FastAPI server. You point it at the directory your build tool produced and it handles serving files, client-side routing fallback, and 404s for you. It landed in [PR #15800](https://github.com/fastapi/fastapi/pull/15800) and comes in two forms: `app.frontend()` on the application, and `router.frontend()` on an `APIRouter`. The full signature is small: ```python app.frontend(path, directory, fallback="auto", check_dir=True) ``` That's it. `path` is the URL prefix to serve from, `directory` is your build output folder, `fallback` controls what happens for unmatched paths, and `check_dir` decides whether the directory must exist when the app boots. Before this, you reached for Starlette's `StaticFiles` and bolted on your own catch-all. Now the framework owns that job. ## How do you serve a built React SPA with app.frontend()? Build your frontend to a folder, then point `app.frontend()` at it with the URL path you want to serve it from. That is the entire setup. Say you have a Vite-powered React app. You build it the usual way, which drops static files into `dist`: ```bash npm run build # Vite writes the compiled SPA into ./dist ``` Then in your FastAPI app, define your API as normal and add one line to serve the SPA: ```python from fastapi import FastAPI app = FastAPI() @app.get("/api/health") def health(): return {"status": "ok"} # serve the built SPA from ./dist at the root app.frontend("/", directory="dist") ``` Start the server and `/` serves your `index.html`, `/assets/index-abc123.js` serves the real compiled file, and `/api/health` still returns JSON. No `StaticFiles`, no manual catch-all, no ordering puzzle. The supported build outputs cover the usual suspects: React with Vite, TanStack Router, Astro, Vue, Svelte and SvelteKit, Angular, and Solid. If your tool produces a folder of static files, `app.frontend()` can serve it. ## How does FastAPI route between your API and the frontend? FastAPI checks your API path operations first, then looks for a matching file in the frontend directory, and only then applies the fallback. Your routes always win, so mounting the frontend at `/` never shadows `/api/...`. ![Animated decision diagram. An incoming request first checks API path operations, then a real file in the frontend directory, then the fallback file, in that order](/images/blogs/serve-react-spa-fastapi-app-frontend-routing-precedence.svg) Walk through three requests against the app above. A `GET /api/health` matches a path operation, so the handler runs and you get JSON. A `GET /assets/index-abc123.js` matches no route but is a real file in `dist`, so FastAPI streams that file. A `GET /dashboard` matches no route and no file, because `/dashboard` is a client-side route that only exists inside your React Router config, so it hits the fallback. That three-step order is the whole mental model. Routes, then files, then fallback. This is why you can serve the frontend at the root without paranoia. The old worry with a catch-all route was that it might intercept an API call you added later. Here, path operations are always consulted first, so a new `/api/users` endpoint keeps working even though the frontend also lives at `/`. ## What do the fallback modes do? The `fallback` argument controls what FastAPI serves when a request matches no API route and no real file. It takes four values: `"auto"` (the default), `"index.html"`, `"404.html"`, or `None`. ![Comparison matrix of the four app.frontend fallback modes, auto, index.html, 404.html, and None, showing what each returns for a missing navigation path versus a missing static asset](/images/blogs/serve-react-spa-fastapi-app-frontend-fallback-modes.svg) Here is what each one does for a browser navigation request to a path that has no file: - **`"auto"`**: serves `404.html` with a 404 status if that file exists, otherwise serves `index.html` for the navigation, otherwise returns a normal 404. This is the sensible default for most SPAs. - **`"index.html"`**: serves `index.html` for the missing path. This is the explicit client-side routing mode, so a deep link or refresh on `/dashboard` loads your app and lets React Router take over. - **`"404.html"`**: serves your custom `404.html` with a real 404 status code, which suits content sites that ship a static not-found page. - **`None`**: no fallback at all. Missing paths just return a normal 404. One rule cuts across all of them: missing static assets like JavaScript, CSS, or images always return a real 404, no matter the fallback. The fallback only applies to browser navigation requests, not to a bad `