2

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
Chris
  • 18,724
  • 6
  • 46
  • 80
ntg
  • 12,950
  • 7
  • 74
  • 95
  • 2
    `root_path` does _not change the application prefix path_. It only instructs the swagger ui/openapi schema to prefix every request with a root path because _a proxy in between the client and the service_ strips away that part (i.e. the request is being mapped between the schemes _by a proxy_ in between). Use `api = APIRouter(prefix="/api/v1"), app.include_router(api)` for example if you want to have a prefix to your routes. You can also give the prefix when calling `include_router` to make the router independent of its mounting point (`include_router(api, prefix="/api/v1")` iirc.) – MatsLindh Apr 14 '23 at 07:45

1 Answers1

3

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 the root_path parameter when creating your FastAPI app:

app = FastAPI(root_path="/api/v1")

Option 1

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.

Working Example

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

Option 2

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_paths 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.

Working Example

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)
Chris
  • 18,724
  • 6
  • 46
  • 80