How do I serve a React-built front-end on a FastAPI backend?

9.6k views Asked by At

I've tried to mount the frontend to / with app.mount, but this invalidates all of my /api routes. I've also tried the following code to mount the folders in /static to their respective routes and serving the index.html file on /:

@app.get("/")
def index():
    project_path = Path(__file__).parent.resolve()
    frontend_root = project_path / "client/build"
    return FileResponse(str(frontend_root) + '/index.html', media_type='text/html')

static_root = project_path / "client/build/static"
app.mount("/static", StaticFiles(directory=static_root), name="static")

This mostly works, but files contained in the client/build folder aren't mounted and are thus inaccessible. I know that Node.js has a way of serving the front-end page with relative paths with res.sendFile("index.html", { root: </path/to/static/folder });. Is there an equivalent function for doing this in FastAPI?

3

There are 3 answers

4
Ricardo On BEST ANSWER

clmno's solution is two servers + routing. Jay Jay Cayabyab is looking for an endpoint on the API that serves a webpacked SPA, the kind you get after npm run build. I was looking for the exact same solution, because that's what I'm doing with Flask and I'm trying to replace Flask with FastAPI.

Following FastAPI's documentation, it is mentioned multiple times that it's based on starlette. Searching for serving a SPA on starlette, I fount this reply to an issue. Of course, this did not work off the shelf for me because I was missing some import, unmentioned in the proposed solution.

Here's my code, and it is working:

from fastapi.staticfiles import StaticFiles

class SPAStaticFiles(StaticFiles):
async def get_response(self, path: str, scope):
    response = await super().get_response(path, scope)
    if response.status_code == 404:
        response = await super().get_response('.', scope)
    return response

app.mount('/my-spa/', SPAStaticFiles(directory='folder', html=True), name='whatever')

Note: I changed the names of endpoint (my-spa), directory (folder) and app name(whatever) on purpose to highlight the point that these need not be all the same.

In this case, you put the built SPA in the folder folder. for this to work, in the SPA project folder, you run npm run build or yarn run build, and you get a folder called dist. Copy all files and folders from dist into this folder folder.

Once you did this, run your FastAPI app and then go to http://localhost:5000/my-spa/. For the sake of absolute clarity, the reason why I'm using this particular URL is that my app has a main like this:

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=5000)

so it starts off port 5000. Your case might differ.

I hate it when imports are missing from these replies, because it sometimes seems like the reply was never even run. Mine is running on the other screen as I type this, so it's not a waste of your time. However, I might be missing some import myself, assuming you're already doing the trivial

from fastapi import FastAPI

and such. If you try this and find anything missing, please let me know here.

0
Michelle Hlcn On

I think you can change the proxy in package.json by adding this line "proxy" at the end of the file. For example, your react runs on localhost:3000 whilst your fastapi son localhost:8000

    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  },
  "proxy": "http://localhost:8000"
}
1
Mike Chaliy On

Update to the answer by Ricardo,

At some point starlette.staticfiles.StaticFiles started raising HTTPException instead of PlaintText 404 response, so for hosting SPA, I guess new version of code should look like this:

from fastapi import HTTPException
from starlette.exceptions import HTTPException as StarletteHTTPException

# ... 

class SPAStaticFiles(StaticFiles):
    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


app.mount("/", SPAStaticFiles(directory="dist", html=True), name="spa-static-files")