4

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?

  • Read [this](https://github.com/Buuntu/fastapi-react) code where `FastAPI` and `React` are served from a single server. The gist is - you need a process running React and a process running FastAPI and a proxy server (nginx in this example) that routes calls. – clamentjohn Oct 23 '20 at 06:17

3 Answers3

12

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.

Ricardo
  • 576
  • 8
  • 10
  • 1
    I found your answer very helpful, I will also say I ended up just doing what was suggested here. https://stackoverflow.com/a/68488252/3538107. thanks for the help! – bravosierra99 Aug 29 '21 at 23:24
  • This is amazingly in-depth, however unfortunately I'm getting ` TypeError: 'NoneType' object is not callable` I believe on `if response.status_code == 404:` , but only for actualy 404s. Very weird, looking into it. – Caleb Jay Oct 23 '21 at 07:07
  • Hi Caleb. As I said, I posted this answer first of all to help _you_. Please contact me on LinkedIn (link on my StackOverflow profile) and let's take it from there. We could have a zoom or something. No, this is not a paid service! – Ricardo Oct 24 '21 at 11:51
5

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:

class SPAStaticFiles(StaticFiles):
    async def get_response(self, path: str, scope):
        try:
            return await super().get_response(path, scope)
        except HTTPException 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="app")
Mike Chaliy
  • 25,801
  • 18
  • 67
  • 105
  • This should be marked as accepted, Ricardo's answer is no longer up-to-date in 2023. @Mike Chaliy Could you please add the import statement `from starlette.exceptions import HTTPException` to your sample? There are two more sources for the `HTTPException` class available in the scope if you're using FastAPI and this is the one that `StaticFiles` actually throws. – PhilJay Aug 22 '23 at 09:13
0

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"
}