Let's say you also have the endpoints dedicated to handling "items" from your application in the module at app/routers/items.py.
You have path operations for:
/items/
/items/{item_id}
It's all the same structure as with app/routers/users.py.
But we want to be smarter and simplify the code a bit.
We know all the path operations in this module have the same:
Path prefix: /items.
tags: (just one tag: items).
Extra responses.
dependencies: they all need that X-Token dependency we created.
So, instead of adding all that to each path operation, we can add it to the APIRouter.
app/routers/items.py
fromfastapiimportAPIRouter,Depends,HTTPExceptionfrom..dependenciesimportget_token_headerrouter=APIRouter(prefix="/items",tags=["items"],dependencies=[Depends(get_token_header)],responses={404:{"description":"Not found"}},)fake_items_db={"plumbus":{"name":"Plumbus"},"gun":{"name":"Portal Gun"}}@router.get("/")asyncdefread_items():returnfake_items_db@router.get("/{item_id}")asyncdefread_item(item_id:str):ifitem_idnotinfake_items_db:raiseHTTPException(status_code=404,detail="Item not found")return{"name":fake_items_db[item_id]["name"],"item_id":item_id}@router.put("/{item_id}",tags=["custom"],responses={403:{"description":"Operation forbidden"}},)asyncdefupdate_item(item_id:str):ifitem_id!="plumbus":raiseHTTPException(status_code=403,detail="You can only update the item: plumbus")return{"item_id":item_id,"name":"The great Plumbus"}
As the path of each path operation has to start with /, like in:
We can also add a list of tags and extra responses that will be applied to all the path operations included in this router.
And we can add a list of dependencies that will be added to all the path operations in the router and will be executed/solved for each request made to them.
Having dependencies in the APIRouter can be used, for example, to require authentication for a whole group of path operations. Even if the dependencies are not added individually to each one of them.
Check
The prefix, tags, responses, and dependencies parameters are (as in many other cases) just a feature from FastAPI to help you avoid code duplication.
This code lives in the module app.routers.items, the file app/routers/items.py.
And we need to get the dependency function from the module app.dependencies, the file app/dependencies.py.
So we use a relative import with .. for the dependencies:
app/routers/items.py
fromfastapiimportAPIRouter,Depends,HTTPExceptionfrom..dependenciesimportget_token_headerrouter=APIRouter(prefix="/items",tags=["items"],dependencies=[Depends(get_token_header)],responses={404:{"description":"Not found"}},)fake_items_db={"plumbus":{"name":"Plumbus"},"gun":{"name":"Portal Gun"}}@router.get("/")asyncdefread_items():returnfake_items_db@router.get("/{item_id}")asyncdefread_item(item_id:str):ifitem_idnotinfake_items_db:raiseHTTPException(status_code=404,detail="Item not found")return{"name":fake_items_db[item_id]["name"],"item_id":item_id}@router.put("/{item_id}",tags=["custom"],responses={403:{"description":"Operation forbidden"}},)asyncdefupdate_item(item_id:str):ifitem_id!="plumbus":raiseHTTPException(status_code=403,detail="You can only update the item: plumbus")return{"item_id":item_id,"name":"The great Plumbus"}
If you know perfectly how imports work, continue to the next section below.
A single dot ., like in:
from.dependenciesimportget_token_header
would mean:
Starting in the same package that this module (the file app/routers/items.py) lives in (the directory app/routers/)...
find the module dependencies (an imaginary file at app/routers/dependencies.py)...
and from it, import the function get_token_header.
But that file doesn't exist, our dependencies are in a file at app/dependencies.py.
Remember how our app/file structure looks like:
The two dots .., like in:
from..dependenciesimportget_token_header
mean:
Starting in the same package that this module (the file app/routers/items.py) lives in (the directory app/routers/)...
go to the parent package (the directory app/)...
and in there, find the module dependencies (the file at app/dependencies.py)...
and from it, import the function get_token_header.
That works correctly! 🎉
The same way, if we had used three dots ..., like in:
from...dependenciesimportget_token_header
that would mean:
Starting in the same package that this module (the file app/routers/items.py) lives in (the directory app/routers/)...
go to the parent package (the directory app/)...
then go to the parent of that package (there's no parent package, app is the top level 😱)...
and in there, find the module dependencies (the file at app/dependencies.py)...
and from it, import the function get_token_header.
That would refer to some package above app/, with its own file __init__.py, etc. But we don't have that. So, that would throw an error in our example. 🚨
But now you know how it works, so you can use relative imports in your own apps no matter how complex they are. 🤓
Add some custom tags, responses, and dependencies¶
We are not adding the prefix /items nor the tags=["items"] to each path operation because we added them to the APIRouter.
But we can still add moretags that will be applied to a specific path operation, and also some extra responses specific to that path operation:
app/routers/items.py
fromfastapiimportAPIRouter,Depends,HTTPExceptionfrom..dependenciesimportget_token_headerrouter=APIRouter(prefix="/items",tags=["items"],dependencies=[Depends(get_token_header)],responses={404:{"description":"Not found"}},)fake_items_db={"plumbus":{"name":"Plumbus"},"gun":{"name":"Portal Gun"}}@router.get("/")asyncdefread_items():returnfake_items_db@router.get("/{item_id}")asyncdefread_item(item_id:str):ifitem_idnotinfake_items_db:raiseHTTPException(status_code=404,detail="Item not found")return{"name":fake_items_db[item_id]["name"],"item_id":item_id}@router.put("/{item_id}",tags=["custom"],responses={403:{"description":"Operation forbidden"}},)asyncdefupdate_item(item_id:str):ifitem_id!="plumbus":raiseHTTPException(status_code=403,detail="You can only update the item: plumbus")return{"item_id":item_id,"name":"The great Plumbus"}
Tip
This last path operation will have the combination of tags: ["items", "custom"].
And it will also have both responses in the documentation, one for 404 and one for 403.
You import and create a FastAPI class as normally.
And we can even declare global dependencies that will be combined with the dependencies for each APIRouter:
app/main.py
fromfastapiimportDepends,FastAPIfrom.dependenciesimportget_query_token,get_token_headerfrom.internalimportadminfrom.routersimportitems,usersapp=FastAPI(dependencies=[Depends(get_query_token)])app.include_router(users.router)app.include_router(items.router)app.include_router(admin.router,prefix="/admin",tags=["admin"],dependencies=[Depends(get_token_header)],responses={418:{"description":"I'm a teapot"}},)@app.get("/")asyncdefroot():return{"message":"Hello Bigger Applications!"}
Now we import the other submodules that have APIRouters:
app/main.py
fromfastapiimportDepends,FastAPIfrom.dependenciesimportget_query_token,get_token_headerfrom.internalimportadminfrom.routersimportitems,usersapp=FastAPI(dependencies=[Depends(get_query_token)])app.include_router(users.router)app.include_router(items.router)app.include_router(admin.router,prefix="/admin",tags=["admin"],dependencies=[Depends(get_token_header)],responses={418:{"description":"I'm a teapot"}},)@app.get("/")asyncdefroot():return{"message":"Hello Bigger Applications!"}
As the files app/routers/users.py and app/routers/items.py are submodules that are part of the same Python package app, we can use a single dot . to import them using "relative imports".
Starting in the same package that this module (the file app/main.py) lives in (the directory app/)...
look for the subpackage routers (the directory at app/routers/)...
and from it, import the submodule items (the file at app/routers/items.py) and users (the file at app/routers/users.py)...
The module items will have a variable router (items.router). This is the same one we created in the file app/routers/items.py, it's an APIRouter object.
the router from users would overwrite the one from items and we wouldn't be able to use them at the same time.
So, to be able to use both of them in the same file, we import the submodules directly:
app/main.py
fromfastapiimportDepends,FastAPIfrom.dependenciesimportget_query_token,get_token_headerfrom.internalimportadminfrom.routersimportitems,usersapp=FastAPI(dependencies=[Depends(get_query_token)])app.include_router(users.router)app.include_router(items.router)app.include_router(admin.router,prefix="/admin",tags=["admin"],dependencies=[Depends(get_token_header)],responses={418:{"description":"I'm a teapot"}},)@app.get("/")asyncdefroot():return{"message":"Hello Bigger Applications!"}
Now, let's include the routers from the submodules users and items:
app/main.py
fromfastapiimportDepends,FastAPIfrom.dependenciesimportget_query_token,get_token_headerfrom.internalimportadminfrom.routersimportitems,usersapp=FastAPI(dependencies=[Depends(get_query_token)])app.include_router(users.router)app.include_router(items.router)app.include_router(admin.router,prefix="/admin",tags=["admin"],dependencies=[Depends(get_token_header)],responses={418:{"description":"I'm a teapot"}},)@app.get("/")asyncdefroot():return{"message":"Hello Bigger Applications!"}
Info
users.router contains the APIRouter inside of the file app/routers/users.py.
And items.router contains the APIRouter inside of the file app/routers/items.py.
With app.include_router() we can add each APIRouter to the main FastAPI application.
It will include all the routes from that router as part of it.
Technical Details
It will actually internally create a path operation for each path operation that was declared in the APIRouter.
So, behind the scenes, it will actually work as if everything was the same single app.
Check
You don't have to worry about performance when including routers.
This will take microseconds and will only happen at startup.
So it won't affect performance. ⚡
Include an APIRouter with a custom prefix, tags, responses, and dependencies¶
Now, let's imagine your organization gave you the app/internal/admin.py file.
It contains an APIRouter with some admin path operations that your organization shares between several projects.
For this example it will be super simple. But let's say that because it is shared with other projects in the organization, we cannot modify it and add a prefix, dependencies, tags, etc. directly to the APIRouter:
But we still want to set a custom prefix when including the APIRouter so that all its path operations start with /admin, we want to secure it with the dependencies we already have for this project, and we want to include tags and responses.
We can declare all that without having to modify the original APIRouter by passing those parameters to app.include_router():
app/main.py
fromfastapiimportDepends,FastAPIfrom.dependenciesimportget_query_token,get_token_headerfrom.internalimportadminfrom.routersimportitems,usersapp=FastAPI(dependencies=[Depends(get_query_token)])app.include_router(users.router)app.include_router(items.router)app.include_router(admin.router,prefix="/admin",tags=["admin"],dependencies=[Depends(get_token_header)],responses={418:{"description":"I'm a teapot"}},)@app.get("/")asyncdefroot():return{"message":"Hello Bigger Applications!"}
That way, the original APIRouter will keep unmodified, so we can still share that same app/internal/admin.py file with other projects in the organization.
The result is that in our app, each of the path operations from the admin module will have:
The prefix /admin.
The tag admin.
The dependency get_token_header.
The response 418. 🍵
But that will only affect that APIRouter in our app, not in any other code that uses it.
So, for example, other projects could use the same APIRouter with a different authentication method.
We can also add path operations directly to the FastAPI app.
Here we do it... just to show that we can 🤷:
app/main.py
fromfastapiimportDepends,FastAPIfrom.dependenciesimportget_query_token,get_token_headerfrom.internalimportadminfrom.routersimportitems,usersapp=FastAPI(dependencies=[Depends(get_query_token)])app.include_router(users.router)app.include_router(items.router)app.include_router(admin.router,prefix="/admin",tags=["admin"],dependencies=[Depends(get_token_header)],responses={418:{"description":"I'm a teapot"}},)@app.get("/")asyncdefroot():return{"message":"Hello Bigger Applications!"}
and it will work correctly, together with all the other path operations added with app.include_router().
Very Technical Details
Note: this is a very technical detail that you probably can just skip.
The APIRouters are not "mounted", they are not isolated from the rest of the application.
This is because we want to include their path operations in the OpenAPI schema and the user interfaces.
As we cannot just isolate them and "mount" them independently of the rest, the path operations are "cloned" (re-created), not included directly.