#231: Split a FastAPI Application Into Manageable Parts
As we saw with the OAuth2 and JWT example, our main.py file can grow quickly. If we keep adding features, the file will be unmaintainable in no time. For Flask we used Blueprint to split up the application , for FastAPI we can use APIRouter to do the same.
fromtypingimportListfromfastapiimportDepends,FastAPI,HTTPException,Request,Response,statusfromfastapi.encodersimportjsonable_encoderfromfastapi.responsesimportJSONResponsefrom.models.todoimport*from.data.datastoreimportDataStoreapp=FastAPI()db=DataStore()asyncdeffilter_parameters(q:str|None=None,include_done:bool=True,due_before:date=date.today()+timedelta(days=365)):return{"q":q,"include_done":include_done,"due_before":due_before}@app.get("/",include_in_schema=False)asyncdefmain():return{'message':'The minimalistic ToDo API'}@app.get("/api/todo")asyncdefshow_all_tasks(filter:Annotated[dict,Depends(filter_parameters)])->List[TaskOutput]:result=db.all()ifnotfilter["include_done"]:result=[itemforiteminresultifitem.done==False]result=[itemforiteminresultifitem.due_date<=filter["due_before"]]returnresult@app.post("/api/todo",status_code=status.HTTP_201_CREATED)asyncdefcreate_task(task:TaskInput,request:Request)->TaskOutput:result=db.add(task)headers={"Location":f"{request.base_url}api/todo/{result.id}"}returnJSONResponse(content=jsonable_encoder(result),status_code=status.HTTP_201_CREATED,headers=headers)@app.get("/api/todo/{id}")asyncdefshow_task(id:int)->TaskOutput:result=db.get(id)ifresult:returnresultelse:raiseHTTPException(status_code=404,detail="Task not found")@app.put("/api/todo/{id}")asyncdefupdate_task(id:int,task:TaskInput)->TaskOutput:try:result=db.update(id,task)returnresultexceptValueError:raiseHTTPException(status_code=404,detail="Task not found")@app.delete("/api/todo/{id}",status_code=status.HTTP_204_NO_CONTENT)asyncdefdelete_task(id:int)->None:db.delete(id)returnResponse(status_code=status.HTTP_204_NO_CONTENT)
Create the router
To move our existing endpoints into a new router, we can follow this checklist:
Create a new folder routers.
Create the empty __init__.py inside routers.
Create a todo.py inside routers.
Copy your existing endpoints (all except main()) to routers/todo.py.
fromdatetimeimportdate,timedeltafromtypingimportAnnotated,ListfromfastapiimportAPIRouter,Depends,HTTPException,Request,Response,statusfromfastapi.encodersimportjsonable_encoderfromfastapi.responsesimportJSONResponsefrom..models.todoimportTaskOutput,TaskInputfrom..data.datastoreimportDataStorerouter=APIRouter()db=DataStore()asyncdeffilter_parameters(q:str|None=None,include_done:bool=True,due_before:date=date.today()+timedelta(days=365)):return{"q":q,"include_done":include_done,"due_before":due_before}@router.get("/api/todo")asyncdefshow_all_tasks(filter:Annotated[dict,Depends(filter_parameters)])->List[TaskOutput]:result=db.all()ifnotfilter["include_done"]:result=[itemforiteminresultifitem.done==False]result=[itemforiteminresultifitem.due_date<=filter["due_before"]]returnresult@router.post("/api/todo",status_code=status.HTTP_201_CREATED)asyncdefcreate_task(task:TaskInput,request:Request)->TaskOutput:result=db.add(task)headers={"Location":f"{request.base_url}api/todo/{result.id}"}returnJSONResponse(content=jsonable_encoder(result),status_code=status.HTTP_201_CREATED,headers=headers)@router.get("/api/todo/{id}")asyncdefshow_task(id:int)->TaskOutput:result=db.get(id)ifresult:returnresultelse:raiseHTTPException(status_code=404,detail="Task not found")@router.put("/api/todo/{id}")asyncdefupdate_task(id:int,task:TaskInput)->TaskOutput:try:result=db.update(id,task)returnresultexceptValueError:raiseHTTPException(status_code=404,detail="Task not found")@router.delete("/api/todo/{id}",status_code=status.HTTP_204_NO_CONTENT)asyncdefdelete_task(id:int)->None:db.delete(id)returnResponse(status_code=status.HTTP_204_NO_CONTENT)
Include the router in main.py
In the main.py file, we can remove the extracted endpoints, get rid of all no longer needed imports and tell FastAPI that we want to include our todo router:
fromfastapiimportDepends,FastAPIfrom.routersimporttodoapp=FastAPI()app.include_router(todo.router)@app.get("/",include_in_schema=False)asyncdefmain():return{'message':'The minimalistic ToDo API'}
Run the tests
We only moved code around inside our application. Therefore, all our tests should still work, and we better check if this is indeed the case:
We currently have /api/todo in the route of all our endpoints. We can add this part as a prefix to the include_router() method in main.py and remove it from the endpoints in our router:
fromfastapiimportFastAPIfrom.routersimporttodoapp=FastAPI()app.include_router(todo.router,prefix="/api/todo")@app.get("/",include_in_schema=False)asyncdefmain():return{'message':'The minimalistic ToDo API'}
We can run our tests to check that everything still works.
This allows us to change the route for our endpoints at a single place, should that be something we want to do in the future.
Attention: If you set the prefix in include_router() and in APIRouter(), the two prefixes get combined.
Next
With APIRouter we not only can separate endpoints by topic, but we can also add endpoints from other packages. That can be helpful for large applications or when we want to use a library to handle all the authentication for us. Next week we extend the to-do application and add a database.