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 FastAPIfrom fastapi.responses import FileResponseapp = 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 FastAPIfrom fastapi.staticfiles import StaticFilesapp = 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
- FastAPI: Render HTML Pages with Jinja2 TemplateResponse in 5 Minutes
- FastAPI: let clients download files safely with FileResponse, blocking path traversal in 5 minutes
- FastAPI: write class-based dependencies — group shared parameters and reusable logic in 5 minutes
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.
