Photo by Stephanie LeBlanc on Unsplash
In the previous story of this series, I tried to made a brief intro to FastAPI framework by summing up the key components and features of it. In this one, an open-source project developed with FastAPI will both be examined and extended for further understanding.
I was too tight on time schedule to build up a microservice architecture application from scratch, therefore, started to look for an existing project to use as a boilerplate. baranbartu’s microservices-with-fastapi was the best choice among the many projects. This is because, first, microservices were implemented to talk via a gateway which is close to many production use cases. Second, authentication and authorization were implemented by using JWT’s. Third, the project was relatively simple to grasp and prototype.
Since the project is a bit outdated, I started with updating the dependencies within requirements files. Later on, on top of the existing project, I added supplier microservice as the third one. The resulting topology is given below. From this point on, I’ll explain the project as a whole without discriminating the parts I added and original ones.
Basic topology of the application:
💡 Serving microservices from AWS APIGW using ALB host header routing:
Serving Microservices from AWS APIGW using ALB host header routing
👉 To read more such acrticles, sign up for free on Differ.
Some Key Files and their Functions
gateway/core.py is the file that most of the logic was implemented. It is basically a wrapper around FastAPI.router and takes on authentication and authorization by checking JWT’s (users microservice handles the verification of username & password and returns if a request is qualified for obtaining JWT). gateway/auth.py provides needed functions to core.py during this process. gateway/main.py is where the journey starts and it consumes the wrapper and its dependencies.
Authentication and authorization logic inside core.py:
Another main function of gateway/core.py is that it reverse-proxies the requests by crafting the header and body accordingly if they’re good to go (i.e have a valid JWT). For this, it utilizes network.make_request method. To send the request, gateway/network.py and gateway/post_processing.py were used. core.py is also responsible of proxying the answer from path operation function to client.
make_request method reverse-proxies the requests to the actual path operation functions:
Another thing I loved about the design of gateway is that wrapper function is developed with flexibility to comply with path operation functions’ needs. For instance, authentication_required parameter determines if a valid JWT required to consume the destination path operation function. It also has comprehensive docs to provide information about each parameter.
Wrapped route function’s parameters:
The Suppliers Microservice
After talking about the gateway, let’s see what the actual microservices do. As all three are similar, I’ll talk about the one I developed which is suppliers.
Before talking about the code within suppliers microservice, we need to create a starting point within gateway’s main.py. This is because the only entry point for all microservices is the gateway. Below exists a hypothetical authentication logic such that, while creating a supplier you need authentication. However, to query existing suppliers, you don’t need to be authenticated. As I said, this logic is arbitrary and may vary accordingly.
Gateway’s path operation functions for suppliers microservice that will run on wrapped route function:
init.py promotes this directory to a Python package. That’s all there is to it.
init_db.py handles the relational database needs. To do that, it utilizes Tortoise ORM and Sqlite3. Since this application has no real use rather than educative purposes, data will not persist and only live in memory. Microservices need to use databases independent of each other. Note that this project complies with it such that each microservice has some form of database (orders and suppliers → sqlite3, users → a json file) for its own.
init_db.py file to fulfill relational database needs:
models.py is housing the pydantic models which is required to use inside path operation functions. Tortoise ORM’s pydantic_model_creator method can transform a pydantic model into a relational database table which makes things quite handy and fast.
from pydantic import BaseModel
from tortoise import fields, models
from tortoise.contrib.pydantic import pydantic_model_creator
class Suppliers(models.Model):
id = fields.IntField(pk=True)
name = fields.TextField()
surname = fields.TextField()
address = fields.TextField()
created_by = fields.IntField()
created_at = fields.DatetimeField(auto_now_add=True)
Supplier_Pydantic = pydantic_model_creator(Suppliers, name='Supplier')
class SupplierIn_Pydantic(BaseModel):
name: str
surname: str
address: str
main.py is the file which receives the request from the gateway, processes it and returns the response. There will be two path operation functions for two different HTTP methods on the same API endpoint (/api/suppliers). First one will be the GET which returns all existing suppliers. And the second one is POST which creates a supplier with creator’s (JWT owner) id.
from typing import List
from fastapi import FastAPI, Header
from tortoise.contrib.fastapi import register_tortoise
from models import Supplier_Pydantic, SupplierIn_Pydantic, Suppliers
app = FastAPI()
@app.get('/api/suppliers', response_model=List[Supplier_Pydantic])
async def get_suppliers():
return await Supplier_Pydantic.from_queryset(Suppliers.all())
@app.post('/api/suppliers', response_model=Supplier_Pydantic)
async def create_supplier(supplier: SupplierIn_Pydantic,
request_user_id: str = Header(None)):
data = supplier.dict()
data.update({'created_by': request_user_id})
supplier_obj = await Suppliers.create(**data)
return await Supplier_Pydantic.from_tortoise_orm(supplier_obj)
register_tortoise(
app,
db_url='sqlite://:memory:',
modules={'models': ['models']},
generate_schemas=True,
add_exception_handlers=True,
)
That’s all for suppliers microservice. Let’s see it in action for a better view. In the below screencast, I’ll obtain an admin token by providing username and password to /api/login. That means, this microservice will return me a JWT. Then, by using this JWT, I’ll create a supplier by sending a POST request to suppliers microservice. After that, I’ll try to create another user without providing the JWT in the request to demonstrate authentication error. Finally, I will send a GET request to supplier microservice to gather information about the supplier that previously created.
This video shows you how to obtain JWT and use it to create suppliers. Then, querying existing ones without it:
Fast API Microservices with JWT authentication
That’s it for this series. My fork of the original GitHub repo can be found here. To conclude, FastAPI is a great framework bringing both functionality and performance to REST API development in Python. Needless to say, this demo was an entry-level one and real-world use cases would be much more complicated and advanced. Anyway, I hope this two stories find you well and helps you build handy APIs.
PS: If you liked the article, please support it by sharing it with others. Cheers!