Based on a few FastAPI tutorials, including this, I made a simple FastAPI app:
from fastapi import FastAPI, Request
app = FastAPI() # also tried FastAPI(root_path="/api/v1")
@app.get("/app")
def read_main(request: Request):
return {"message": "Hello World", "root_path": request.scope.get("root_path")}
Which i want to have at a path other than root (e.g. /api/vi)... Again based on most tutorials and common sense, I tried to start it with e.g.:
uvicorn main:app --root-path /api/v1
The service comes up ok (on http://127.0.0.1:8000
), however, the root-path
seems to be ignored: Any GET
request to http://127.0.0.1:8000/
gives:
message "Hello World"
root_path "/api/v1"
and any GET
request to http://127.0.0.1:8000/api/v1
gives:
detail "Not Found"
I would expect the requests to produce the reverse outcomes... What is going on here?!?
I also tried initializing FastAPI with FastAPI(root_path="/api/v1")
as well as switching to hypercorn without avail...
Details of the versions of apps (I might have tried a few others as well, though these should be the latest tried):
python 3.9.7 hf930737_3_cpython conda-forge
fastapi 0.85.1 pyhd8ed1ab_0 conda-forge
uvicorn 0.20.0 py39h06a4308_0
hypercorn 0.14.3 py39hf3d152e_1 conda-forge
As noted by @MatsLindh in the comments section, root_path
(or --root-path
) does not change your application's prefix path, but is rather designated for behind the proxy cases, where "you might need to use a proxy server like Traefik or Nginx with a configuration that adds an extra path prefix that is not seen by your application" (see the relevant documentation).
As described in the documentation:
Proxy with a stripped path prefix
Having a proxy with a stripped path prefix, in this case, means that you could declare a path at
/app
in your code, but then, you add a layer on top (the proxy) that would put your FastAPI application under a path like/api/v1
.In this case, the original path
/app
would actually be served at/api/v1/app
.Even though all your code is written assuming there's just
/app
.And the proxy would be "stripping" the path prefix on the fly before transmitting the request to Uvicorn, keep your application convinced that it is serving at
/app
, so that you don't have to update all your code to include the prefix/api/v1
.Up to here, everything would work as normally.
But then, when you open the integrated docs UI (the frontend), it would expect to get the OpenAPI schema at
/openapi.json
, instead of/api/v1/openapi.json
.So, the frontend (that runs in the browser) would try to reach
/openapi.json
and wouldn't be able to get the OpenAPI schema (it would show "Failed to load API definition" error).Because we have a proxy with a path prefix of
/api/v1
for our app, the frontend needs to fetch the OpenAPI schema at/api/v1/openapi.json
. The docs UI would also need the OpenAPI schema to declare that this API server is located at/api/v1
(behind the proxy).To achieve this, you can use the command line option
--root-path
:uvicorn main:app --root-path /api/v1
[...]
Alternatively, if you don't have a way to provide a command line option like
--root-path
or equivalent, you can set theroot_path
parameter when creating your FastAPI app:app = FastAPI(root_path="/api/v1")
Hence, in your case, since you are not using a proxy, but rather need to have a custom prefix for your API, you could instead use an APIRouter
, which allows you to define a prefix
for the API routes (note that the prefix
must not include a final /
). You can either give the prefix
when instantiating the APIRouter
(e.g., router = APIRouter(prefix='/api/v1')
) or using .include_router()
, which, as described in the documentation, would allow you to include the same router multiple times with different prefix:
You can also use
.include_router()
multiple times with the same router using different prefixes.This could be useful, for example, to expose the same API under different prefixes, e.g.
/api/v1
and/api/latest
.This is an advanced usage that you might not really need, but it's there in case you do.
The /app
endpoint in the example below can be accessed at http://127.0.0.1:8000/api/v1/app.
from fastapi import FastAPI
from fastapi.routing import APIRouter
router = APIRouter()
@router.get('/app')
def main():
return 'Hello world!'
app = FastAPI()
app.include_router(router, prefix='/api/v1')
Once you have multiple versions of the API endpoints, you could use:
from fastapi import FastAPI
from fastapi.routing import APIRouter
router_v1 = APIRouter()
router_v2 = APIRouter()
@router_v1.get('/app')
def main():
return 'Hello world - v1'
@router_v2.get('/app')
def main():
return 'Hello world - v2'
app = FastAPI()
app.include_router(router_v1, prefix='/api/v1')
app.include_router(router_v2, prefix='/api/v2')
app.include_router(router_v2, prefix='/latest') # optional
Alternatively, one could also mount sub-application(s) with the desired prefix
, as demonstrated in this answer and this answer (see Option 3).
As described in the documentation:
When you mount a sub-application as described above, FastAPI will take care of communicating the mount path for the sub-application using a mechanism from the ASGI specification called a
root_path
.That way, the sub-application will know to use that path prefix for the docs UI.
And the sub-application could also have its own mounted sub-applications and everything would work correctly, because FastAPI handles all these
root_path
s automatically.
Hence, in the example given below, you can access the /app
endpoint from the main app at http://127.0.0.1:8000/app and the /app
endpoint from the sub app at http://127.0.0.1:8000/api/v1/app. Similarly, the Swagger UI autodocs can be accessed at http://127.0.0.1:8000/docs and http://127.0.0.1:8000/api/v1/docs, respectively.
from fastapi import FastAPI
app = FastAPI()
subapi = FastAPI()
@app.get('/app')
def read_main():
return {'message': 'Hello World from main app'}
@subapi.get('/app')
def read_sub():
return {'message': 'Hello World from sub API'}
app.mount('/api/v1', subapi)