This is a log of my fastapi backend development.
Requirements:
- A basic CRUD
- Structure code and data for flexibility, so that I can keep on adding features w/o changing the old code a lot. Minor changes are OK
- Authentication and authorization
basics:
- we shoudl create a virtual environment before coding this project
- this will help us create a sandbox where we have our own python version and project dependencies
- Command: python3 -m venv env
- it will create a virtual environment folder named “env” in our root folder which will contain all our dependencies
- we can activate using command: source env/bin/activate
- I am using Mac, so the command are like this, windows has different, but similar to this, simple google should work
- After activating, we are inside the sanbox environment
- Install fastapi thru the following commands:
- pip install fastapi
- this installs the core fastapi with no server, data validations, data processing, we can manually add later
- pip install “fastapi[standard]”
- this provides all of the things out of the box so that we can start development
- pip install fastapi
- the purpose of the first one is, we can have freedom to use different servers like hypercorn, granian, etc. Also, it is best for microservices where we dont need anything extra
- think like, first one is the node.js and the second one is express.js, if you are from javascript domain
route/controller layer:
I wanted to make versioning of routes like api/v1/books and extend to as many version I want so that my results can change as per the version if I want to upgrade or my data/requirements hets changed in future.
I might use the same data as well for newer versions, so I want to separate the service layer from this layer. So, that I can have both versions available but can restrict or allow accrdingly.
my_project/└── src/ └── routes/ └── __init__.py └── v1/ ├── __init__.py └── books_routes.py
As, it is visible from the diagram, that we can have multiple version folders in the routes folder and inside each of the folder we can have different routes/controllers. This is what I exactly wanted.
routing problem
But, the issue is now, we need to register all these routes inside the main.py file where we have initiated the server. We cant keep all the routes in the same file also I want my routes to be separated as per its features
from fastapi import FastAPIversion = 'v1'app = FastAPI( title="Bookly", version=version)# Include routes hereapp.get("/")def home(): return {"status": "server online"}
routing Solution:
For every folder in the project, there is __init__.py, which tells the interpreter that its a folder/package, from where we can import files. This gets called exactly once for application lifecycle for the very first time it gets imported somewhere and then cached it in sys.modules. It is also used as index.js where we can import all the files from folder and later can be imported directly from this file in some other file.
I defined 2 functions in the __init__.py file of the routes folder:
- Discover all the routes inside the folder including every version folder
- Register all of them as a route to the the app
import importlibimport pkgutilfrom fastapi import APIRouter, FastAPIdef discover_versioned_routers() -> dict[str, list[APIRouter]]: # Scans version directories and returns a mapping of versions to routers version_mapping = {} for _, v_dir, is_pkg in pkgutil.iter_modules(__path__): if is_pkg: version_mapping[v_dir] = [] v_package_path = f"{__name__}.{v_dir}" v_package = importlib.import_module(v_package_path) for _, mod_name, is_sub_pkg in pkgutil.iter_modules(v_package.__path__): if not is_sub_pkg: module = importlib.import_module(f".{mod_name}", package=v_package_path) if hasattr(module, "router") and isinstance(module.router, APIRouter): version_mapping[v_dir].append(module.router) return version_mappingdef init_app_routes(app: FastAPI) -> None: # 🎯 ROUTE REGISTRATION FUNCTION: Registers all discovered routes into FastAPI. versioned_map = discover_versioned_routers() for version, routers in versioned_map.items(): for sub_router in routers: app.include_router( sub_router, prefix=f"/api/{version}" )
In discover_versioned_routers(), we are getting all the version folders on the first loop and in the internal 2nd loop, we are checking for the name “router”(this is important) which should be an instance of APIRouter, then we are adding it the dict called version_mapping. Every route we define in the file inside version folders like v1, v2, etc., should be using “router”, thats the catch
Then, in the scond one, we are just registering the route with the “app”, that is passed through the function as parameter.
If we would not have done this, we would have done like this:
from fastapi import FastAPIfrom src.routes.v1 import books_routesfrom src.routes.v1 import authors_routesfrom src.routes.v1 import publications_routesapp = FastAPI()app.include_router(books_routes, prefix="/api/v1/books")app.include_router(authors_routes, prefix="/api/v1/authors")app.include_router(publications_routes, prefix="/api/v1/publications")# .# .# .# .# so on...# Similarly for v2 in future
The file would get bulkier with more code. Now the file can be something like this, if we use our __init__.py function here. Continuing from the earlier main.py file, not the above one.
from src.routes import init_app_routes# more code above ⬆️# Include routes hereinit_app_routes(app)# more code below ⬇️
This fixes our routing layer’s registering issue. Let’s see the routing file itself.
from fastapi import APIRouterrouter = APIRouter(prefix="/books", tags=['Books v1'])router.get("/")async def get_all_books(): # Call to service function and get the books # books = service_func_call() # return {"message": "Books fetched succesfully", "data": books}
This seems alright. So lets build the service layer. But, before building the service layer, we need a DB layer that will provide us the access to DB, so that our service can execute queries.
Leave a comment