Fastapi: [QUESTION] Url parameters in nested routers?

Created on 15 Apr 2019  ·  11Comments  ·  Source: tiangolo/fastapi

I'm developing a public api where we'll probably want to nest routes at least a couple of levels deep, using the initial route to determine the 'app' that the rest of the content belongs to.

our target url structure is along the lines of

/api/v1/apps/{shortcode}
/api/v1/apps/{shortcode}/people/
/api/v1/apps/{shortcode}/people/{id}

etc. etc...

I'd really like to take advantage of the APIRouter classes to do this as we're likely to have a lot of endpoints and models but I can't find a way to ensure the 'shortcode' parameter here is passed to child routers, is there any way to do that?

currently I'm doing stuff like this:

app.py

from app.people.routes import router as people_router
from app.routes import router as main_router

app = FastAPI()
app.debug = settings.DEBUG

main_router.include_router(people_router, prefix='/{shortcode}/people')
app.include_router(main_router, prefix='/apps')

This works ok, but the {shortcode} parameter is not included in the auto-generated documentation ( though it is available in the view functions ). I'd rather not have to define every view function individually with prefixed like @router.route('/{shortcode}/people/{id}) ... if I can avoid it somehow?

question

Most helpful comment

Ah great thankyou I'll give it a try

All 11 comments

Hmm, interesting. I hadn't seen a use case like this, let's see...

Are you declaring a parameter shortcode in each one of those path operation functions? Are you declaring it with a specific Python type?

Thanks for looking at this :) Yeah the idea would be that all the 'sub-routers' would need access to the {shortcode} parameter to be able to perform initial database filtering of their content type.

it would be declared just as a str type, however it could be useful to be able to wrap it in a Depends() to be able to retrieve an actual model in the views where we know we'll need to query it from the database along with the actual content for that view

I've gotten around this for now by just declaring the whole url path including the {shortcode} parameter in the sub routers and attaching them at the app root. This works ok for the moment and is no great loss but I can imagine this may be useful further down the line for much larger url structures.

I see how this could be useful.

I'll have to check it to debug it and see what's blocking it and how feasible it would be to support it.

I see how this could be useful.

I'll have to check it to debug it and see what's blocking it and how feasible it would be to support it.

how is it going ? it's very import concept for restful nested resources.

@bharling I just added tests for this specific use case in #349, I see it working as expected.

And I see it documented on the API docs:

Selection_066

Please check with a recent version and let me know if you still have any problem.

Ah great thankyou I'll give it a try

@tiangolo This is mostly great thanks - I can see the url parameters appearing in my routes now and all kind of checks out apart from the rendering of the documentation - it seems to be doubling up all the routes. I expect I'm doing something wrong - here's a screenshot of what I see in the docs ( both redoc and swagger versions have the same issue )

image

The 'People' endpoints there are also all duplicated under a 'People' tag below that app list.

Here's the code I'm using to build the routing at the app level

# we prefix with the api version number
v1_router = APIRouter()

# /v1/apps/{shortcode}/people/
app_detail_router.include_router(people_router, tags=['People'], prefix='/people')

# /v1/apps/{shortcode} - App Detail ( and all app sub routes )
app_list_router.include_router(app_detail_router, prefix='/{shortcode}', tags=['App'])

# /v1/apps - Apps List
v1_router.include_router(app_list_router, tags=['App'], prefix='/apps')

# /v1
app.include_router(v1_router, prefix='/v1')

app.include_router(auth_router, tags=['Authentication'])

UPDATE:

Following the pattern below works well though however. Doing the below is fine for my purposes definitely. I think my initial issue was because of nesting tagged routes which may actually be a misuse of the openapi spec perhaps anyhow.

example_router = APIRouter()

@example_router.get('/example')
def example_get(shortcode:str):
    return {'shortcode': shortcode }

@example_router.get('/example/{id}')
def example_detail(shortcode:str, id:int):
    return {'shortcode': shortcode, 'id': id}


v1_router = APIRouter()
v1_router.include_router(auth_router, tags=['Authentication'])
v1_router.include_router(app_router, tags=['Apps'])
v1_router.include_router(images_router, tags=['Images'], prefix='/{shortcode}')
v1_router.include_router(people_router, tags=['People'], prefix='/{shortcode}')
v1_router.include_router(example_router, tags=['Example'], prefix='/{shortcode}')

app.include_router(v1_router, prefix='/v1/apps')

I'd be happy to close this now on the basis that the above works for me, but maybe others may have input.

Great, thanks for reporting back and closing the issue.

Having multiple tags is actually not a misuse of the spec, but the IU and code generation tools tend to assume that each path operation appears only in one tag, or that it's ok for them to appear with each tag 🤷‍♂️😅... There might be cases where you want a path operation to appear in different tags.

But ReDoc showing them multiple times instead of once with all the independent tags seems like a ReDoc bug.

I'd like to add to this seeing as how I can't figure out how to get this to work properly. Let's say I have an API as follows:

GET /board - Lists all board entries
POST /board - Adds a new board entry
GET /board/{board_id} - Gets a specific board entry

GET /board/{board_id}/defect - Gets all defects related to the specified board
POST /board/{board_id}/defect - Adds a new defect entry to the specified board
GET /board/{board_id}/defect/{defect_id} - Gets the specified defect belonging to the specified board

This is pretty much core REST API design, and I would expect to be able to dig down to an arbitrary depth in the same way, where each path parameter identifies the preceding resource in the path, but I don't really see from the description above or the documentation how to achieve this. If I try for example to do

api_router.include_router(boards.router, prefix="/board", tags=["board"])
api_router.include_router(boards.router, prefix="/board/{board_id}", tags=["board"])

@router.get("/", response_model=List[Board])
def read_boards():
return crud.boards.getAll()

@router.get("/", response_model=Board)
def read_board(boardId):
return crud.boards.getOne(board_id)

then doing a GET on /board/15 will assume I am trying to reach "getAll()" and going for a deeper resource (defect) won't work at all.

Cheers

Was this page helpful?
0 / 5 - 0 ratings