Sometimes you want to serve a modern frontend - like a React, Vue, or Next.js app - alongside your FastAPI backend. This keeps everything in one place and makes deployment easier. You don't need a separate web server for your frontend; FastAPI can handle it.
Hosting your frontend and backend together means you only have one service to deploy and manage. You avoid CORS headaches, simplify your infrastructure, and keep your API and UI tightly integrated. For small projects or internal tools, this approach is practical and saves time.
CORS is implemented as a middleware in FastAPI. To learn more about other types of middleware and why their order is important, check out my guide on adding middleware to FastAPI applications.
ORIGINS = ["http://localhost", "http://localhost:5173"] ALLOW_METHODS = ["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"] ALLOW_HEADERS = ["Authorization", "Content-Type", "Accept", "Cookie", "Origin"] # Initialize the FastAPI app app = FastAPI( title="My FastAPI App", description="APIs for managing my FastAPI data.", version="1.0.0", redoc_url=None, openapi_url=None, docs_url=None, on_startup=[on_start_up], on_shutdown=[on_shutdown], swagger_favicon_url="/static/assets/favicon.ico", swagger_ui_parameters={"defaultModelsExpandDepth": -1}, ) # 1. Add CORS Middleware FIRST. # This ensures it's applied to all responses, including errors. app.add_middleware( CORSMiddleware, allow_origins=ORIGINS, allow_credentials=True, allow_methods=ALLOW_METHODS, allow_headers=ALLOW_HEADERS, max_age=7200, # Cache preflight responses for 2 hours (7200 seconds) )
ORIGINS
lists allowed sources for cross-origin requests. http://localhost:5173
is common for local frontend development (for example, Vite or React dev servers).If your backend and frontend are hosted on the same machine and accessed from the same origin, CORS settings are less important because requests are not considered cross-origin. But if you serve your frontend and backend from different domains or ports (like during development), you need to set the correct origins to avoid browser errors.ALLOW_METHODS
specify which HTTP methods and headers are permitted. This keeps your API secure and flexible.ALLOW_HEADERS
lists which HTTP headers are allowed in requests to your API.
Include "Authorization"
so users can send authentication tokens, "Content-Type"
and "Accept"
for specifying and negotiating data formats, "Cookie"
for session management, and "Origin"
for CORS checks. These headers are commonly needed for secure APIs and modern frontend apps to work correctly with authentication, JSON payloads, and cross-origin requests.Single Page Applications (SPAs) handle routing in the browser. If a user visits /dashboard
directly, the server should serve index.html
so the frontend router (like React Router) can display the correct page. Without this, the server would return a 404 error for unknown routes. To fix this, we tell FastAPI to always serve index.html
for any path that isn't an API route or a static file.
Here's how you can do that with a custom StaticFiles
class:
from fastapi.staticfiles import StaticFiles from starlette.exceptions import HTTPException as StarletteHTTPException # Define a subclass of StaticFiles to create a custom static file handler # that supports client-side routing in Single Page Applications (SPAs). class SPAStaticFiles(StaticFiles): # Override the get_response method, which is responsible for retrieving # static file responses for given paths. async def get_response(self, path: str, scope): try: return await super().get_response(path, scope) except (HTTPException, StarletteHTTPException) as ex: if ex.status_code == 404: return await super().get_response("index.html", scope) else: raise ex # Mount the SPAStaticFiles instance at the root URL ("/"). # This means any request not caught by other routes (like the API) will be handled here. # - 'directory' specifies where your built static files (from Next.js) reside. # - 'html=True' tells FastAPI to serve HTML files by default. # - 'name' is an identifier for this mounted application app.mount("/", SPAStaticFiles(directory="app/frontend/dist/", html=True), name="spa")
This means any request not handled by your API routes will be served by your frontend.
Tip: Make sure you create a custom 404 page in your React application. This way, when a user visits an invalid route, your frontend will display a helpful "Page Not Found" message instead of a blank screen. The SPA handler will always serve index.html
, so your React router should catch unknown paths and render your 404 component.
If you want to deploy your FastAPI backend and single page frontend together, you can use a multi-stage Docker build. This lets you build your Python dependencies, compile your frontend static files, and serve everything from one container.
Here’s a real-world example Dockerfile and what each stage does:
# Stage 1: Build Python dependencies and backend code FROM python:3.13.5-alpine3.22 AS builder RUN apk add --no-cache build-base libffi-dev openssl-dev musl-dev postgresql-dev WORKDIR /build COPY requirements.txt . RUN pip install --prefix=/install --no-cache-dir -r requirements.txt COPY app ./app # Stage 2: Build frontend static files FROM node:20-alpine AS frontend-builder WORKDIR /frontend COPY app/frontend/package.json app/frontend/package-lock.json ./ RUN npm ci COPY app/frontend ./ ARG VITE_BACKEND_HOST ENV VITE_BACKEND_HOST=$VITE_BACKEND_HOST RUN npm run build # Stage 3: Final runtime image FROM python:3.13.5-alpine3.22 RUN apk add --no-cache libffi openssl ffmpeg postgresql-libs WORKDIR /app # Copy Python dependencies and app code COPY --from=builder /install /usr/local COPY --from=builder /build/app ./app # Copy built frontend static files into the backend's expected location COPY --from=frontend-builder /frontend/dist ./app/frontend/dist # (Optional) Compile Python bytecode RUN python -m compileall -q app # Set environment variable for port (optional) ENV PORT=8080 CMD exec gunicorn --bind :$PORT --workers 1 --worker-class uvicorn.workers.UvicornWorker -t 3600 app.main:app
How it works:
Stage 1 (builder): Installs all Python dependencies and copies your backend code. This stage uses Alpine Linux for a smaller image.
Stage 2 (frontend-builder): Uses Node.js to install frontend dependencies and build your static files (like React, Vue, or Next.js). The built files end up in /frontend/dist
.
You’ll notice these lines in the Dockerfile:
ARG VITE_BACKEND_HOST ENV VITE_BACKEND_HOST=$VITE_BACKEND_HOST
This sets the VITE_BACKEND_HOST environment variable during the frontend build. The value is embedded into the generated static files, so your frontend knows where to send API requests. In local development, you might set this to something like http://localhost:5000
(your FastAPI backend). In production, you can leave it blank, since both the backend and frontend are served from the same container and origin.
Stage 3 (final runtime): Starts from a fresh Python Alpine image, installs only the runtime libraries, and copies in both the backend and the built frontend. The static files are placed where FastAPI expects them, so your SPA is served correctly.
This approach keeps your deployment simple and ensures your frontend and backend are always in sync. You only need to run one container for both your API and your SPA.
Want to learn more about building slimmer Docker images for FastAPI projects? Check out my guide: Slimmer FastAPI Docker Images with Multi-Stage Builds
With this setup, FastAPI serves both your API and your frontend. You get a single deployment, simple routing, and fewer moving parts. The middleware order keeps your app secure and efficient, and the SPA handler makes client-side routing work smoothly.
Serving a Single Page Application with FastAPI is straightforward. You don't need extra web servers or complicated setups. Just build your frontend, drop the files in the right directory, and let FastAPI handle the rest. Make sure your middleware is in the right order, and use a custom static file handler for smooth routing. This approach works well for many projects, but if your app grows, you might want to split frontend and backend for scalability.
David Muraya is a Solutions Architect specializing in Python, FastAPI, and Cloud Infrastructure. He is passionate about building scalable, production-ready applications and sharing his knowledge with the developer community. You can connect with him on LinkedIn.
Enjoyed this blog post? Check out these related posts!
Managing Background Tasks in FastAPI: BackgroundTasks vs ARQ + Redis
A practical guide to background processing in FastAPI, comparing built-in BackgroundTasks with ARQ and Redis for scalable async job queues.
Read More..
Optimizing Reflex Performance on Google Cloud Run
A Comparison of Gunicorn, Uvicorn, and Granian for Running Reflex Apps
Read More..
Building a Flexible Memcached Client for FastAPI
Flexible, Safe, and Efficient Caching for FastAPI with Memcached and aiomcache
Read More..
6 Essential FastAPI Middlewares for Production-Ready Apps
A guide to the 6 key middlewares for building secure, performant, and resilient FastAPI applications.
Read More..
Have a project in mind? Send me an email at hello@davidmuraya.com and let's bring your ideas to life. I am always available for exciting discussions.