When FastAPI needs to serve static files — images, CSS, JavaScript, or a frontend build — the right way is to mount StaticFiles once with app.mount(), instead of writing one endpoint per file. StaticFiles handles reading the file, setting Content-Type from the extension, and returning 404 when a file does not exist. This applies when you serve your own application’s public assets from a dedicated folder that contains no source code or sensitive config files.

The problem

A small app often starts with one endpoint returning the logo, then adds one for the favicon, one CSS file, one image at a time. Every new file means a new function, a new route, and renaming a file means editing code. Worse, if you read files yourself with open() and return a Response, you must handle Content-Type, block ../ paths escaping the folder, and handle missing files — all things StaticFiles already does for you.

from fastapi import FastAPI
from fastapi.responses import FileResponse
app = FastAPI()
@app.get("/assets/logo.png")
def logo():
return FileResponse("static/logo.png")
@app.get("/assets/style.css")
def style():
return FileResponse("static/style.css")

The solution

Mount StaticFiles once, and every file in the folder gets a URL immediately with no extra route:

from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
app = FastAPI()
app.mount("/static", StaticFiles(directory="static"), name="static")

Now static/logo.png is available at /static/logo.png, and a file copied into the folder is reachable right away, with no code change or server restart. A missing file returns 404, and a request like /static/../app.py is also blocked with 404 because the resolved path must stay inside the configured folder.

Two important notes. First, mount order: register API routes first and mount at / last — otherwise a / mount will “swallow” API routes, because routes are checked in registration order. With html=True, StaticFiles returns index.html for a directory path, and returns 404.html (if present) for paths that match no file:

@app.get("/api/ping")
def ping():
return {"ok": True}
app.mount("/static", StaticFiles(directory="static"), name="static")
app.mount("/", StaticFiles(directory="frontend/dist", html=True), name="frontend")

Second, the folder must exist at startup: by default StaticFiles raises a RuntimeError immediately if the folder is missing, instead of silently returning 404 for every request. Pass check_dir=False only when the folder is created dynamically at runtime.

Applying it to your project

In a real project, keep a dedicated static folder containing only public assets, and never mount the project root: a wrong mount would expose source code, .env files, and configuration to the internet. A common layout: the API stays under /api, the frontend build folder (for example frontend/dist) mounts at / with html=True so the homepage serves immediately, plus a 404.html pointing back to the SPA entry if client-side sub-routes need support. For large files or heavy traffic, put Nginx or a CDN in front of the static folder and let FastAPI handle only the API.

Related posts

FAQ

How is StaticFiles different from FileResponse?

FileResponse returns one specific file from one endpoint, handy when you need per-file access control via an auth dependency. StaticFiles serves a whole folder through a single mount, right when the files are all public and numerous.

What does StaticFiles return for a missing file?

404. In html=True mode, a directory path returns index.html, while a path matching no file returns 404.html if present, otherwise a plain 404.

Do I need to restart the server after adding a file?

No. StaticFiles reads files per request, so a newly copied file is reachable immediately.

Can I use StaticFiles in production?

Yes, for small and medium apps. Under heavy traffic, serving files from Python consumes app workers, so let Nginx or a CDN serve the static folder instead.

💬 Have you tried this in your own project? If you hit an error or know a cleaner way, leave a comment below — I read them all and will fold good ideas into the post.

Sources