2

Exploring the Core Features

In the previous chapter, we found out how easy it is to install and start developing REST APIs using the FastAPI framework. Handling requests, cookies, and form data was fast, easy, and straightforward with FastAPI, as was building the different HTTP path operations.

To learn about the framework’s features further, this chapter will guide us on how to upgrade our REST APIs by adding some essential FastAPI features to the implementation. These include some handlers that can help minimize unchecked exceptions, JSON encoders that can directly manage endpoint responses, background jobs that can create audit trails and logs, and multiple threads to run some API methods asynchronously with the uvicorn’s main thread. Moreover, issues such as managing source files, modules, and packages for huge enterprise projects will also be addressed in this chapter. This chapter will use and dissect an intelligent tourist system prototype to assist with elaborating upon and exemplifying FastAPI’s core modules.

Based on these aforementioned features, this chapter will discuss the following major concepts that can help us extend our learning about this framework:

  • Structuring and organizing huge projects
  • Managing API-related exceptions
  • Converting objects to JSON-compatible types
  • Managing API responses
  • Creating background processes
  • Using asynchronous path operations
  • Applying middleware to filter path operations

Technical requirements

This chapter will implement a prototype of an intelligent tourist system designed to provide booking information and reservation about tourist spots. It can provide user details, tourist spot details, and location grids. It also allows users or tourists to comment on tours and rate them. The prototype has an administrator account for adding and removing all the tour details, managing users, and providing some listings. The application will not use any database management system yet, so all the data is temporarily stored in Python collections. The code is all uploaded at https://github.com/PacktPublishing/Building-Python-Microservices-with-FastAPI/tree/main/ch02.

Structuring and organizing huge projects

In FastAPI, big projects are organized and structured by adding packages and modules without destroying the setup, configuration, and purpose. The project should always be flexible and scalable in case of additional features and requirements. One component must correspond to one package, with several modules equivalent to a blueprint in a Flask framework.

In this prototypical intelligent tourist system, the application has several modules such as the login, administration, visit, destination, and feedback-related functionalities. The two most crucial are the visit module, which manages all the travel bookings of the users, and the feedback module, which enables clients to post their feedback regarding their experiences at every destination. These modules should be separated from the rest since they provide the core transactions. Figure 2.1 shows how to group implementations and separate a module from the rest using packages:

Figure 2.1 – The FastAPI project structure

Figure 2.1 – The FastAPI project structure

Each package in Figure 2.1 contains all the modules where the API services and some dependencies are implemented. All the aforementioned modules now have their own respective packages that make it easy to test, debug, and expand the application. Testing FastAPI components will be discussed in the upcoming chapters.

Important note

FastAPI does not require adding the __init__.py file into each Python package when using VS Code Editor and Python 3.8 during development, unlike in Flask. The __pycache__ folder generated inside a package during compilation contains binaries of the module scripts accessed and utilized by other modules. The main folder will also become a package since it will have its own __pycache__ folder together with the others. But we must exclude __pycache__ when deploying the application to the repository, since it may take up a lot of space.

On the other hand, what remains in the main folder are the core components such as the background tasks, custom exception handlers, middleware, and the main.py file. Now, let us learn about how FastAPI can bundle all these packages as one huge application when deployed.

Implementing the API services

For these module packages to function, the main.py file must call and register all their API implementations through the FastAPI instance. The scripts inside each package are already REST API implementations of the microservices, except that they are built by APIRouter instead of the FastAPI object. APIRouter also has the same path operations, query and request parameter setup, handling of form data, generation of responses, and parameter injection of model objects. What is lacking in APIRouter is the support for an exception handler, middleware declaration, and customization:

from fastapi import APIRouter
from login.user import Signup, User, Tourist, 
      pending_users, approved_users
router = APIRouter()
@router.get("/ch02/admin/tourists/list")
def list_all_tourists():
    return approved_users

The list_all_tourists() API method operation here is part of the manager.py module in the admin package, implemented using APIRouter due to project structuring. The method returns a list of tourist records that are allowed to access the application, which can only be provided by the user.py module in the login package.

Importing the module components

Module scripts can share their containers, BaseModel classes, and other resource objects to other modules using Python’s from… import statement. Python’s from… import statement is better since it allows us to import specific components from a module, instead of including unnecessary ones:

from fastapi import APIRouter, status
from places.destination import Tour, TourBasicInfo, 
    TourInput, TourLocation, tours, tours_basic_info, 
    tours_locations
router = APIRouter()
@router.put("/ch02/admin/destination/update", 
            status_code=status.HTTP_202_ACCEPTED)
def update_tour_destination(tour: Tour):
    try:
        tid = tour.id
        tours[tid] = tour
        tour_basic_info = TourBasicInfo(id=tid, 
           name=tour.name, type=tour.type, 
           amenities=tour.amenities, ratings=tour.ratings)
        tour_location = TourLocation(id=tid, 
           name=tour.name, city=tour.city, 
           country=tour.country, location=tour.location )
        tours_basic_info[tid] = tour_basic_info
        tours_locations[tid] = tour_location
        return { "message" : "tour updated" }
    except:
        return { "message" : "tour does not exist" } 

The update_tour_destination() operation here will not work without importing the Tour, TourBasicInfo, and TourLocation model classes from destination.py in the places package. It shows the dependency between modules that happens when structuring is imposed on big enterprise web projects.

Module scripts can also import components from the main project folder when needed by the implementation. One such example is accessing the middleware, exception handlers, and tasks from the main.py file.

Important note

Avoid cycles when dealing with the from… import statement. A cycle happens when a module script, a.py, accesses components from b.py that import resource objects from a.py. FastAPI does not accept this scenario and will issue an error message.

Implementing the new main.py file

Technically, the project’s packages and its module scripts will not be recognized by the framework unless their respective router object is added or injected into the application’s core through the main.py file. main.py, just as the other project-level scripts do, uses FastAPI and not APIRouter to create and register components, as well as the package’s modules. The FastAPI class has an include_router() method that adds all these routers and injects them into the framework to make them part of the project structure. Beyond registering the routers, this method can also add other attributes and components to the router such as URL prefixes, tags, dependencies such as exception handlers, and status codes:

from fastapi import FastAPI, Request
from admin import manager
from login import user
from feedback import post
from places import destination
from tourist import visit
app = FastAPI()
app.include_router(manager.router)
app.include_router(user.router)
app.include_router(destination.router)
app.include_router(visit.router)
app.include_router(
    post.router,
    prefix="/ch02/post"
)

This code is the main.py implementation of the intelligent tourist system prototype tasked to import all the registers of the module’s scripts from the different packages, before adding them as components to the framework. Run the application using the following command:

uvicorn main:app –-reload

This will allow you to access all the APIs of these modules at http://localhost:8000/docs.

What happens to the application when API services encounter runtime problems during execution? Is there a way to manage these problems besides applying Python’s try-except block? Let us explore implementing API services with exception-handling mechanisms further.

Managing API-related exceptions

The FastAPI framework has a built-in exception handler derived from its Starlette toolkit that always returns default JSON responses whenever HTTPException is encountered during the execution of the REST API operation. For instance, accessing the API at http://localhost:8000/ch02/user/login without providing the username and password will give us the default JSON output depicted in Figure 2.2:

Figure 2.2 – The default exception result

Figure 2.2 – The default exception result

In some rare cases, the framework sometimes chooses to return the HTTP response status instead of the default JSON content. But developers can still opt to override these default handlers to choose which responses to return whenever a specific exception cause happens.

Let us now explore how to formulate a standardized and appropriate way of managing runtime errors in our API implementation.

A single status code response

One way of managing the exception-handling mechanism of your application is to apply a try-except block to manage the return responses of your API when it encounters an exception or none. After applying try-block, the operation should trigger a single status code, most often Status Code 200 (SC 200). The path operation of FastAPI and APIRouter has a status_code parameter that we can use to indicate the type of status code we want to raise.

In FastAPI, status codes are integer constants that are found in the status module. It also allows integer literals to indicate the needed status code if they are a valid status code number.

Important Note

A status code is a 3-digit number that indicates a reason for, information on, or status of the HTTP response of a REST API operation. The status code range 200 to 299 denotes a successful response, 300 to 399 pertains to redirection, 400-499 pertains to client-related problems, and 500 to 599 is related to server errors.

This technique is rarely used because there are times that an operation needs to be clear in recognizing every exception that it encounters, which can only be done by returning HTTPException instead of a custom error message wrapped in a JSON object:

from fastapi import APIRouter, status
@router.put("/ch02/admin/destination/update", 
              status_code=status.HTTP_202_ACCEPTED)
def update_tour_destination(tour: Tour):
    try:
        tid = tour.id
        tours[tid] = tour
        tour_basic_info = TourBasicInfo(id=tid, 
           name=tour.name, type=tour.type, 
           amenities=tour.amenities, ratings=tour.ratings)
        tour_location = TourLocation(id=tid, 
           name=tour.name, city=tour.city, 
           country=tour.country, location=tour.location )
        tours_basic_info[tid] = tour_basic_info
        tours_locations[tid] = tour_location
        return { "message" : "tour updated" }
    except:
        return { "message" : "tour does not exist" }
@router.get("/ch02/admin/destination/list", 
            status_code=200)
def list_all_tours():
    return tours

The list_all_tours() method shown here is the kind of REST API service that should emit Status Code 200 – it gives an error-free result just by rendering the Python collection with data. Observe that the literal integer value, 200, or SC 200, assigned to the status_code parameter of the GET path operation always raises an OK status. On the other hand, the update_tour_destination() method shows another approach in emitting status codes by using a try-except block, wherein both blocks return a custom JSON response. Whichever scenario happens, it will always trigger SC 202, which may not apply to some REST implementations. After the status module is imported, its HTTP_202_ACCEPTED constant is used to set the value of the status_code parameter.

Multiple status codes

If we need each block in try-except to return their respective status code, we need to avoid using the status_code parameter of the path operations and use JSONResponse instead. JSONResponse is one of the FastAPI classes used to render a JSON response to the client. It is instantiated, constructor-injected with values for its content and status_code parameters, and returned by the path operations. By default, the framework uses this API to help path operations render responses as JSON types. Its content parameter should be a JSON-type object, while the status_code parameter can be an integer constant and a valid status code number, or it can be a constant from the module status:

from fastapi.responses import JSONResponse
@router.post("/ch02/admin/destination/add")
add_tour_destination(input: TourInput):
    try:
        tid = uuid1()
        tour = Tour(id=tid, name=input.name,
           city=input.city, country=input.country, 
           type=input.type, location=input.location,
           amenities=input.amenities, feedbacks=list(), 
           ratings=0.0, visits=0, isBooked=False)
        tour_basic_info = TourBasicInfo(id=tid, 
           name=input.name, type=input.type, 
           amenities=input.amenities, ratings=0.0)
        tour_location = TourLocation(id=tid, 
           name=input.name, city=input.city, 
           country=input.country, location=input.location )
        tours[tid] = tour
        tours_basic_info[tid] = tour_basic_info
        tours_locations[tid] = tour_location
        tour_json = jsonable_encoder(tour)
        return JSONResponse(content=tour_json, 
            status_code=status.HTTP_201_CREATED)
    except:
        return JSONResponse(
         content={"message" : "invalid tour"}, 
         status_code=status.HTTP_500_INTERNAL_SERVER_ERROR)

The add_tour_destination() operation here has a try-except block where its try block returns the tour details and SC 201, while its catch block returns an error message inside a JSON-type object with a server error of SC 500.

Raising HTTPException

Another way of managing possible errors is by letting the REST API throw the HTTPException object. HTTPException is a FastAPI class that has required constructor parameters: detail, which needs an error message in the str type, and status_code, which asks for a valid integer value. The detail part is converted to JSON-type and returned to the user as a response after the HTTPException instance is thrown by the operation.

To throw HTTPException, a validation process using any variations of if statements is more appropriate than using the try-except block because the cause of the error needs to be identified before throwing the HTTPException object using the raise statement. Once raise is executed, the whole operation will halt and send the HTTP error message in JSON-type to the client with the specified status code:

from fastapi import APIRouter, HTTPException, status
@router.post("/ch02/tourist/tour/booking/add")
def create_booking(tour: TourBasicInfo, touristId: UUID):
    if approved_users.get(touristId) == None:
         raise HTTPException(status_code=500,
            detail="details are missing")
    booking = Booking(id=uuid1(), destination=tour,
      booking_date=datetime.now(), tourist_id=touristId)
    approved_users[touristId].tours.append(tour)
    approved_users[touristId].booked += 1
    tours[tour.id].isBooked = True
    tours[tour.id].visits += 1
    return booking

The create_booking() operation here simulates a booking process for a tourist account, but before the procedure starts, it first checks whether the tourist is still a valid user; otherwise, it will raise HTTPException, halting all the operations in order to return an error message.

Custom exceptions

It is also possible to create a user-defined HTTPException object to handle business-specific problems. This custom exception requires a custom handler needed to manage its response to the client whenever an operation raises it. These custom components should be available to all API methods across the project structure; thus, they must be implemented at the project-folder level.

In our application, there are two custom exceptions created in handler_exceptions.py, the PostFeedbackException and PostRatingFeedback exceptions, which handle problems related to posting feedback and ratings on a particular tour:

from fastapi import FastAPI, Request, status, HTTPException
class PostFeedbackException(HTTPException):
    def __init__(self, detail: str, status_code: int):
        self.status_code = status_code
        self.detail = detail
        
class PostRatingException(HTTPException):
    def __init__(self, detail: str, status_code: int):
        self.status_code = status_code
        self.detail = detail

A valid FastAPI exception is a subclass of an HTTPException object inheriting the essential attributes, namely the status_code and detail attributes. We need to supply values to these attributes before the path operation raises the exception. After creating these custom exceptions, a specific handler is implemented and mapped to an exception.

The FastAPI @app decorator in main.py has an exception_handler() method, used to define a custom handler and map it to the appropriate custom exception. A handler is simply a Python function with two local parameters, Request and the custom exception that it manages. The purpose of the Request object is to retrieve cookies, payloads, headers, query parameters, and path parameters from the path operation if the handler expects any of this request data. Now, once the custom exception is raised, the handler is set to generate a JSON-type response to the client containing the detail and the status_code attributes provided by the path operation that raised the exception:

from fastapi.responses import JSONResponse
from fastapi import FastAPI, Request, status, HTTPException
@app.exception_handler(PostFeedbackException)
def feedback_exception_handler(req: Request, 
          ex: PostFeedbackException):
    return JSONResponse(
        status_code=ex.status_code,
        content={"message": f"error: {ex.detail}"}
        )
    
@app.exception_handler(PostRatingException)
def rating_exception_handler(req: Request, 
             ex: PostRatingException):
     return JSONResponse(
        status_code=ex.status_code,
        content={"message": f"error: {ex.detail}"}
        )

When an operation in post.py raises PostFeedbackException, the feedback_exception_handler() given here will trigger its execution to generate a response that can provide details about what has caused the feedback problem. The same thing will happen to PostRatingException and its rating_exception_handler():

from handlers import PostRatingException,
                         PostFeedbackException
    
@router.post("/feedback/add")
def post_tourist_feedback(touristId: UUID, tid: UUID, 
      post: Post, bg_task: BackgroundTasks):
    if approved_users.get(touristId) == None and 
          tours.get(tid) == None:
        raise PostFeedbackException(detail='tourist and 
                tour details invalid', status_code=403)
    assessId = uuid1()
    assessment = Assessment(id=assessId, post=post, 
          tour_id= tid, tourist_id=touristId) 
    feedback_tour[assessId] = assessment
    tours[tid].ratings = (tours[tid].ratings + 
                            post.rating)/2
    bg_task.add_task(log_post_transaction, 
           str(touristId), message="post_tourist_feedback")
    assess_json = jsonable_encoder(assessment)
    return JSONResponse(content=assess_json, 
                         status_code=200)
@router.post("/feedback/update/rating")
def update_tour_rating(assessId: UUID, 
               new_rating: StarRating):
    if feedback_tour.get(assessId) == None:
        raise PostRatingException(
         detail='tour assessment invalid', status_code=403)
    tid = feedback_tour[assessId].tour_id
    tours[tid].ratings = (tours[tid].ratings + 
                            new_rating)/2
    tour_json = jsonable_encoder(tours[tid])
    return JSONResponse(content=tour_json, status_code=200)

post_tourist_feedback() and update_tour_rating() here are the API operations that will raise the PostFeedbackException and PostRatingException custom exceptions, respectively, triggering the execution of their handlers. The detail and status_code values injected into the constructor are passed to the handlers to create the response.

A default handler override

The optimum way to override the exception-handling mechanism of your application is to replace the global exception handler of the FastAPI framework that manages its core Starlette’s HTTPException and the RequestValidationError triggered by Pydantic’s request validation process. For instance, if we want to change the response format of the global exception sent to the client using raise from JSON-type to plain text, we can create custom handlers for each of the aforementioned core exceptions that will pursue the format conversion. The following snippets of main.py show these types of custom handlers:

from fastapi.responses import PlainTextResponse 
from starlette.exceptions import HTTPException as 
         GlobalStarletteHTTPException
from fastapi.exceptions import RequestValidationError
from handler_exceptions import PostFeedbackException, 
        PostRatingException
@app.exception_handler(GlobalStarletteHTTPException)
def global_exception_handler(req: Request, 
                 ex: str
    return PlainTextResponse(f"Error message: 
       {ex}", status_code=ex.status_code)
@app.exception_handler(RequestValidationError)
def validationerror_exception_handler(req: Request, 
                 ex: str
    return PlainTextResponse(f"Error message: 
       {str(ex)}", status_code=400)

Both the global_exception_handler() and validationerror_exception_handler() handlers are implemented to change the framework’s JSON-type exception response to PlainTextResponse. An alias, GlobalStarletteHTTPException, is assigned to Starlette’s HTTPException class to distinguish it from FastAPI’s HTTPException, which we previously used to build custom exceptions. On the other hand, PostFeedbackException and PostRatingException are both implemented in the handler_exceptions.py module.

JSON objects are all over the FastAPI framework’s REST API implementation, from the incoming request to the outgoing responses. However, what if the JSON data involved in the process is not a FastAPI JSON-compatible type? The following discussion will expound more upon this kind of object.

Converting objects to JSON-compatible types

It is easier for FastAPI to process JSON-compatible types such as dict, list, and BaseModel objects because they can be easily converted to JSON by the framework using its default JSON editor. However, there are circumstances in which runtime exceptions are raised when processing BaseModel, data model, or JSON objects containing data. One of the many reasons for this is that these data objects have attributes that are not supported by JSON rules, such as UUID and non-built-in date types. Regardless, using a framework’s module classes, these objects can still be utilized by converting them into JSON-compatible ones.

When it comes to the direct handling of the API operation’s responses, FastAPI has a built-in method that can encode typical model objects to convert them to JSON-compatible types before persisting them to any datastore or passing them to the detail parameter of JSONResponse. This method, jsonable_encoder(), returns a dict type with all the keys and values compatible with JSON:

from fastapi.encoders import jsonable_encoder
from fastapi.responses import JSONResponse
class Tourist(BaseModel):
    id: UUID
    login: User
    date_signed: datetime
    booked: int
    tours: List[TourBasicInfo]
    
@router.post("/ch02/user/signup/")
async def signup(signup: Signup):
    try:
        userid = uuid1()
        login = User(id=userid, username=signup.username, 
               password=signup.password)
        tourist = Tourist(id=userid, login=login, 
          date_signed=datetime.now(), booked=0, 
          tours=list() )
        tourist_json = jsonable_encoder(tourist)
        pending_users[userid] = tourist_json
        return JSONResponse(content=tourist_json, 
            status_code=status.HTTP_201_CREATED)
    except:
        return JSONResponse(content={"message": 
         "invalid operation"}, 
         status_code=status.HTTP_500_INTERNAL_SERVER_ERROR)

Our application has a POST operation, signup(), shown here that captures the profile of a newly created user to be approved by the administrator. If you observe the Tourist model class, it has a date_signed attribute that is declared as datettime, and temporal types are not always JSON-friendly. Having model objects with non-JSON-friendly components in FastAPI-related operations can cause serious exceptions. To avoid these Pydantic validation issues, it is always advisable to use jsonable_encoder() to manage the conversion of all the attributes of our model object into JSON-types.

Important note

The json module with its dumps() and loads() utility methods can be used instead of jsonable_encoder() but a custom JSON encoder should be created to successfully map the UUID type, the formatted date type, and other complex attribute types to str.

Chapter 9, Utilizing Other Advanced Features, will discuss other JSON encoders that can encode and decode JSON responses faster than the json module.

Managing API responses

The use of jsonable_encoder() can help an API method not only with data persistency problems but also with the integrity and correctness of its response. In the signup() service method, JSONResponse returns the encoded Tourist model instead of the original object to ensure that the client always received a JSON response. Aside from raising status codes and providing error messages, JSONResponse can also do some tricks in handling the API responses to the client. Although optional in many circumstances, applying the encoder method when generating responses is recommended to avoid runtime errors:

from fastapi.encoders import jsonable_encoder
from fastapi.responses import JSONResponse
@router.get("/ch02/destinations/details/{id}")
def check_tour_profile(id: UUID):
    tour_info_json = jsonable_encoder(tours[id])
    return JSONResponse(content=tour_info_json)

check_tour_profile() here uses JSONResponse to ensure that its response is JSON-compatible and is fetched from the purpose of managing its exceptions. Moreover, it can also be used to return headers together with the JSON-type response:

@router.get("/ch02/destinations/list/all")
def list_tour_destinations():
    tours_json = jsonable_encoder(tours)
    resp_headers = {'X-Access-Tours': 'Try Us', 
       'X-Contact-Details':'1-900-888-TOLL', 
       'Set-Cookie':'AppName=ITS; Max-Age=3600; Version=1'}
    return JSONResponse(content=tours_json, 
          headers=resp_headers)

The application’s list_tour_destinations() here returns three cookies: AppName, Max-Age, and Version, and two user-defined response headers. Headers that have names beginning with X- are custom headers. Besides JSONResponse, the fastapi module also has a Response class that can create response headers:

from fastapi import APIRouter, Response
@router.get("/ch02/destinations/mostbooked")
def check_recommended_tour(resp: Response):
    resp.headers['X-Access-Tours'] = 'TryUs'
    resp.headers['X-Contact-Details'] = '1900888TOLL'
    resp.headers['Content-Language'] = 'en-US'
    ranked_desc_rates = sort_orders = sorted(tours.items(),
         key=lambda x: x[1].ratings, reverse=True)
    return ranked_desc_rates;

Our prototype’s check_recommend_tour() uses Response to create two custom response headers and a known Content-Language. Always remember that headers are all str types and are stored in the browser for many reasons, such as creating an identity for the application, leaving user trails, dropping advertisement-related data, or leaving an error message to the browser when an API encounters one:

@router.get("/ch02/tourist/tour/booked")
def show_booked_tours(touristId: UUID):
    if approved_users.get(touristId) == None:
         raise HTTPException(
         status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 
         detail="details are missing", 
         headers={"X-InputError":"missing tourist ID"})
    return approved_users[touristId].tours

HTTPException, as shown in the show_booked_tours() service method here, not only contains the status code and error message but also some headers in case the operation needs to leave some error information to the browser once it is raised.

Let us now explore the capability of FastAPI to create and manage transactions that are designed to run in the background using some server threads.

Creating background processes

The FastAPI framework is also capable of running background jobs as part of an API service execution. It can even run more than one job almost simultaneously without intervening in the main service execution. The class responsible for this is BackgroundTasks, which is part of the fastapi module. Conventionally, we declare this at the end of the parameter list of the API service method for the framework to inject the BackgroundTask instance.

In our application, the task is to create audit logs of all API service executions and store them in an audit_log.txt file. This operation is part of the background.py script that is part of the main project folder, and the code is shown here:

from datetime import datetime
def audit_log_transaction(touristId: str, message=""):
    with open("audit_log.txt", mode="a") as logfile:
        content = f"tourist {touristId} executed {message} 
            at {datetime.now()}"
        logfile.write(content)

Here, audit_log_transaction() must be injected into the application using BackgroundTasks’s add_task() method to become a background process that will be executed by the framework later:

from fastapi import APIRouter, status, BackgroundTasks
@router.post("/ch02/user/login/")
async def login(login: User, bg_task:BackgroundTasks):
    try:
        signup_json = 
           jsonable_encoder(approved_users[login.id]) 
        bg_task.add_task(audit_log_transaction,
            touristId=str(login.id), message="login")
        return JSONResponse(content=signup_json, 
            status_code=status.HTTP_200_OK)
    except:
        return JSONResponse(
         content={"message": "invalid operation"}, 
         status_code=status.HTTP_500_INTERNAL_SERVER_ERROR)
    
@router.get("/ch02/user/login/{username}/{password}")
async def login(username:str, password: str, 
                    bg_task:BackgroundTasks):
     tourist_list = [ tourist for tourist in 
        approved_users.values() 
          if tourist['login']['username'] == username and 
              tourist['login']['password'] == password] 
     if len(tourist_list) == 0 or tourist_list == None:
        return JSONResponse(
           content={"message": "invalid operation"}, 
           status_code=status.HTTP_403_FORBIDDEN)
     else:
        tourist = tourist_list[0]
        tour_json = jsonable_encoder(tourist)
        bg_task.add_task(audit_log_transaction, 
          touristId=str(tourist['login']['id']), message="login")
        return JSONResponse(content=tour_json, 
            status_code=status.HTTP_200_OK)

The login() service method is just one of the services of our application that logs its details. It uses the bg_task object to add audit_log_transaction() into the framework to be processed later. Transactions such as logging, SMTP-/FTP-related requirements, events, and some database-related triggers are the best candidates for background jobs.

Important Note

Clients will always get their response from the REST API method despite the execution time of the background task. Background tasks are for processes that will take enough time that including them in the API operation could cause performance degradation.

Using asynchronous path operations

When it comes to improving performance, FastAPI is an asynchronous framework, and it uses Python’s AsyncIO principles and concepts to create a REST API implementation that can run separately and independently from the application’s main thread. The idea also applies to how a background task is executed. Now, to create an asynchronous REST endpoint, attach async to the func signature of the service:

@router.get("/feedback/list")
async def show_tourist_post(touristId: UUID):
    tourist_posts = [assess for assess in feedback_tour.values() 
            if assess.tourist_id == touristId]
    tourist_posts_json = jsonable_encoder(tourist_posts) 
    return JSONResponse(content=tourist_posts_json,
                   status_code=200)

Our application has a show_tourist_post() service that can retrieve all the feedback posted by a certain touristId about a vacation tour that they have experienced. The application will not be affected no matter how long the service will take because its execution will be simultaneous to the main thread.

Important Note

The feedback APIRouter uses a /ch02/post prefix indicated in its main.py’s include_router() registration. So, to run show_tourist_post(), the URL should be http://localhost:8000/ch02/post.

An asynchronous API endpoint can invoke both synchronous and asynchronous Python functions that can be DAO (Data Access Object), native services, or utility. Since FastAPI also follows the Async/Await design pattern, the asynchronous endpoint can call an asynchronous non-API operation using the await keyword, which halts the API operation until the non-API transaction is done processing a promise:

from utility import check_post_owner
@router.delete("/feedback/delete")
async def delete_tourist_feedback(assessId: UUID, 
              touristId: UUID ):
    if approved_users.get(touristId) == None and 
            feedback_tour.get(assessId):
        raise PostFeedbackException(detail='tourist and 
              tour details invalid', status_code=403)    post_delete = [access for access in feedback_tour.values()
               if access.id == assessId]
    for key in post_delete:
        is_owner = await check_post_owner(feedback_tour, 
                       access.id, touristId)
        if is_owner:
            del feedback_tour[access.id]
    return JSONResponse(content={"message" : f"deleted
          posts of {touristId}"}, status_code=200)

delete_tourist_feedback() here is an asynchronous REST API endpoint that calls an asynchronous Python function, check_post_owner(), from the utility.py script. For the two components to have a handshake, the API service invokes check_post_owner(), using an await keyword for the former to wait for the latter to finish its validation, and retrieves the promise that it can get from await.

Important Note

The await keyword can only be used with the async REST API and native transactions, not with synchronous ones.

To improve performance, you can add more threads within the uvicorn thread pool by including the --workers option when running the server. Indicate your preferred number of threads after calling the option:

uvicorn main:app --workers 5 --reload

Chapter 8, Creating Coroutines, Events, and Message-Driven Transactions, will discuss the AsyncIO platform and the use of coroutines in more detail.

And now, the last, most important core feature that FastAPI can provide is the middleware or the "request-response filter."

Applying middleware to filter path operations

There are FastAPI components that are inherently asynchronous and one of them is the middleware. It is an asynchronous function that acts as a filter for the REST API services. It filters out the incoming request to pursue validation, authentication, logging, background processing, or content generation from the cookies, headers, request parameters, query parameters, form data, or authentication details of the request body before it reaches the API service method. Equally, it takes the outgoing response body to pursue rendition change, response header updates and additions, and other kinds of transformation that could possibly be applied to the response before it reaches the client. Middleware should be implemented at the project level and can even be part of main.py:

@app.middleware("http")
async def log_transaction_filter(request: Request, 
             call_next):
    start_time = datetime.now()
    method_name= request.method
    qp_map = request.query_parasms
    pp_map = request.path_params
    with open("request_log.txt", mode="a") as reqfile:
        content = f"method: {method_name}, query param: 
            {qp_map}, path params: {pp_map} received at 
            {datetime.now()}"
        reqfile.write(content)
    response = await call_next(request)
    process_time = datetime.now() - start_time
    response.headers["X-Time-Elapsed"] = str(process_time)
    return response

To implement middleware, first, create an async function that has two local parameters: the first one is Request and the second one is a function called call_next(), which takes the Request parameter as its argument to return the response. Then, decorate the method with @app.middleware("http") to inject the component into the framework.

The tourist application has one middleware implemented by the asynchronous add_transaction_filter() here that logs the necessary request data of a particular API method before its execution and modifies its response object by adding a response header, X-Time-Elapsed, which carries the running time of the execution.

The execution of await call_next(request) is the most crucial part of the middleware because it explicitly controls the execution of the REST API service. It is the area of the component where Request passes through to the API execution for processing. Equally, it is where Response tunnels out, going to the client.

Besides logging, middleware can also be used for implementing one-way or two-way authentication, checking user roles and permissions, global exception handling, and other filtering-related operations right before the execution of call_next(). When it comes to controlling the outgoing Response, it can be used to modify the content type of the response, remove some existing browser cookies, modify the response detail and status code, redirections, and other response transformation-related transactions. Chapter 9, Utilizing Other Advanced Features, will discuss the types of middleware, middleware chaining, and other means to customize middleware to help build a better microservice.

Important note

The FastAPI framework has some built-in middleware that is ready to be injected into the application such as GzipMiddleware, ServerErrorMiddleware, TrustedHostMiddleware, ExceptionMiddleware, CORSMiddleware, SessionMiddleware, and HTTPSRedirectionMiddleware.

Summary

Exploring the core details of a framework always helps us create a comprehensive plan and design to build quality applications to the required standards. We have learned that FastAPI injects all its incoming form data, request parameters, query parameters, cookies, request headers, and authentication details into the Request object, and the outgoing cookies, response headers, and response data are carried out to the client by the Response object. When managing the response data, the framework has a built-in jsonable_encoder() function that can convert the model into JSON types to be rendered by the JSONResponse object. Its middleware is one powerful feature of FastAPI because we can customize it to handle the Request object before it reaches the API execution and the Response object before the client receives it.

Managing the exceptions is always the first step to consider before creating a practical and sustainable solution for the resiliency and health of a microservice architecture. Alongside its robust default Starlette global exception handler and Pydantic model validator, FastAPI allows exception-handling customization that provides the flexibility needed when business processes become intricate.

FastAPI follows Python’s AsyncIO principles and standards for creating async REST endpoints, which makes implementation easy, handy, and reliable. This kind of platform is helpful for building complex architectures that require more threads and asynchronous transactions.

This chapter is a great leap toward fully learning about the principles and standards of how FastAPI manages its web containers. The features highlighted in this chapter hitherto open up a new level of knowledge that we need to explore further if we want to utilize FastAPI to build great microservices. In the next chapter, we will be discussing FastAPI dependency injection and how this design pattern affects our FastAPI projects.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.116.64.221