Home Web Development Building Python Microservices with FastAPI

Building Python Microservices with FastAPI

By Sherwin John C. Tragura
books-svg-icon Book
eBook $37.99 $25.99
Print $46.99
Subscription $15.99 $10 p/m for three months
$10 p/m for first 3 months. $15.99 p/m after that. Cancel Anytime!
What do you get with a Packt Subscription?
This book & 7000+ ebooks & video courses on 1000+ technologies
60+ curated reading lists for various learning paths
50+ new titles added every month on new and emerging tech
Early Access to eBooks as they are being written
Personalised content suggestions
Customised display settings for better reading experience
50+ new titles added every month on new and emerging tech
Playlists, Notes and Bookmarks to easily manage your learning
Mobile App with offline access
What do you get with a Packt Subscription?
This book & 6500+ ebooks & video courses on 1000+ technologies
60+ curated reading lists for various learning paths
50+ new titles added every month on new and emerging tech
Early Access to eBooks as they are being written
Personalised content suggestions
Customised display settings for better reading experience
50+ new titles added every month on new and emerging tech
Playlists, Notes and Bookmarks to easily manage your learning
Mobile App with offline access
What do you get with eBook + Subscription?
Download this book in EPUB and PDF formats, plus a monthly download credit
This book & 6500+ ebooks & video courses on 1000+ technologies
60+ curated reading lists for various learning paths
50+ new titles added every month on new and emerging tech
Early Access to eBooks as they are being written
Personalised content suggestions
Customised display settings for better reading experience
50+ new titles added every month on new and emerging tech
Playlists, Notes and Bookmarks to easily manage your learning
Mobile App with offline access
What do you get with a Packt Subscription?
This book & 6500+ ebooks & video courses on 1000+ technologies
60+ curated reading lists for various learning paths
50+ new titles added every month on new and emerging tech
Early Access to eBooks as they are being written
Personalised content suggestions
Customised display settings for better reading experience
50+ new titles added every month on new and emerging tech
Playlists, Notes and Bookmarks to easily manage your learning
Mobile App with offline access
What do you get with eBook?
Download this book in EPUB and PDF formats
Access this title in our online reader
DRM FREE - Read whenever, wherever and however you want
Online reader with customised display settings for better reading experience
What do you get with video?
Download this video in MP4 format
Access this title in our online reader
DRM FREE - Watch whenever, wherever and however you want
Online reader with customised display settings for better learning experience
What do you get with video?
Stream this video
Access this title in our online reader
DRM FREE - Watch whenever, wherever and however you want
Online reader with customised display settings for better learning experience
What do you get with Audiobook?
Download a zip folder consisting of audio files (in MP3 Format) along with supplementary PDF
What do you get with Exam Trainer?
Flashcards, Mock exams, Exam Tips, Practice Questions
Access these resources with our interactive certification platform
Mobile compatible-Practice whenever, wherever, however you want
BUY NOW $10 p/m for first 3 months. $15.99 p/m after that. Cancel Anytime!
eBook $37.99 $25.99
Print $46.99
Subscription $15.99 $10 p/m for three months
What do you get with a Packt Subscription?
This book & 7000+ ebooks & video courses on 1000+ technologies
60+ curated reading lists for various learning paths
50+ new titles added every month on new and emerging tech
Early Access to eBooks as they are being written
Personalised content suggestions
Customised display settings for better reading experience
50+ new titles added every month on new and emerging tech
Playlists, Notes and Bookmarks to easily manage your learning
Mobile App with offline access
What do you get with a Packt Subscription?
This book & 6500+ ebooks & video courses on 1000+ technologies
60+ curated reading lists for various learning paths
50+ new titles added every month on new and emerging tech
Early Access to eBooks as they are being written
Personalised content suggestions
Customised display settings for better reading experience
50+ new titles added every month on new and emerging tech
Playlists, Notes and Bookmarks to easily manage your learning
Mobile App with offline access
What do you get with eBook + Subscription?
Download this book in EPUB and PDF formats, plus a monthly download credit
This book & 6500+ ebooks & video courses on 1000+ technologies
60+ curated reading lists for various learning paths
50+ new titles added every month on new and emerging tech
Early Access to eBooks as they are being written
Personalised content suggestions
Customised display settings for better reading experience
50+ new titles added every month on new and emerging tech
Playlists, Notes and Bookmarks to easily manage your learning
Mobile App with offline access
What do you get with a Packt Subscription?
This book & 6500+ ebooks & video courses on 1000+ technologies
60+ curated reading lists for various learning paths
50+ new titles added every month on new and emerging tech
Early Access to eBooks as they are being written
Personalised content suggestions
Customised display settings for better reading experience
50+ new titles added every month on new and emerging tech
Playlists, Notes and Bookmarks to easily manage your learning
Mobile App with offline access
What do you get with eBook?
Download this book in EPUB and PDF formats
Access this title in our online reader
DRM FREE - Read whenever, wherever and however you want
Online reader with customised display settings for better reading experience
What do you get with video?
Download this video in MP4 format
Access this title in our online reader
DRM FREE - Watch whenever, wherever and however you want
Online reader with customised display settings for better learning experience
What do you get with video?
Stream this video
Access this title in our online reader
DRM FREE - Watch whenever, wherever and however you want
Online reader with customised display settings for better learning experience
What do you get with Audiobook?
Download a zip folder consisting of audio files (in MP3 Format) along with supplementary PDF
What do you get with Exam Trainer?
Flashcards, Mock exams, Exam Tips, Practice Questions
Access these resources with our interactive certification platform
Mobile compatible-Practice whenever, wherever, however you want
  1. Free Chapter
    Chapter 1: Setting Up FastAPI for Starters
About this book
FastAPI is an Asynchronous Server Gateway Interface (ASGI)-based framework that can help build modern, manageable, and fast microservices. Because of its asynchronous core platform, this ASGI-based framework provides the best option when it comes to performance, reliability, and scalability over the WSGI-based Django and Flask. When working with Python, Flask, and Django microservices, you’ll be able to put your knowledge to work with this practical guide to building seamlessly manageable and fast microservices. You’ll begin by understanding the background of FastAPI and learning how to install, configure, and use FastAPI to decompose business units. You’ll explore a unique and asynchronous REST API framework that can provide a better option when it comes to building microservices. After that, this book will guide you on how to apply and translate microservices design patterns in building various microservices applications and RESTful APIs using the FastAPI framework. By the end of this microservices book, you’ll be able to understand, build, deploy, test, and experiment with microservices and their components using the FastAPI framework.
Publication date:
August 2022
Publisher
Packt
Pages
420
ISBN
9781803245966

 

Setting Up FastAPI for Starters

In any software development work, it is always important to first know the business requirement of the project and the appropriate framework, tools, and deployment platform to use before pursuing the task. Frameworks that are easy to understand and use, seamless during coding, and within standards are always picked because of the integrity they provide to solve problems without risking too much development time. And a promising Python framework called FastAPI, created by Sebastian Ramirez, provides experienced developers, experts, and enthusiasts the best option for building REST APIs and microservices.

But before proceeding to the core details of building microservices using FastAPI, it is best to first learn the building blocks of this framework, such as how it captures clients’ requests, how it builds the rules for each HTTP method, and how it manages HTTP responses. Learning the basic components is always essential to know the strengths and weaknesses of the framework and to what extent we can apply FastAPI to solve different enterprise-grade and microservices-related problems.

Thus, in this chapter, we’re going to have a walkthrough of the basic features of FastAPI by covering the following main topics:

  • The setup of the development environment
  • Initialization and configuration of FastAPI
  • Design and implementation of the REST APIs
  • Managing user requests and server response
  • Handling form parameters
  • Handling cookies
 

Technical requirements

The software specimen for this chapter is a prototypical administrator-managed online academic discussion forum, which is an academic discussion hub where alumni, teachers, and students can exchange ideas. The prototype is working but it is open for changes, so you can tweak the code while reading this chapter. It is not designed to use any database management system, but all the data is temporarily stored in various Python collections. All the applications in this book are compiled and run using Python 3.8. Codes are all uploaded at https://github.com/PacktPublishing/Building-Python-Microservices-with-FastAPI/tree/main/ch01.

 

Setting up the development environment

The FastAPI framework is a fast, seamless, and robust Python framework but can only work on Python versions 3.6 and above. The Integrated Development Environment (IDE) used in this reference is Visual Studio Code (VS Code), which is an open source tool that we can download from this site: https://code.visualstudio.com/. Just be sure to install the VSC extensions such as Python, Python for VS Code, Python Extension Pack, Python Indent, and Material Icon Theme to provide your editor syntax checking, syntax highlighting, and other editor support.

After the successful installation of Python and VS Code, we can now install FastAPI using a terminal console. To ensure correct installation, first update Python’s package installer (pip) by running this command:

python -m pip install --upgrade pip

Afterward, we install the framework by running this series of commands:

pip install fastapi
pip install uvicorn[standard]
pip install python-multipart

Important note

If you need to install the complete FastAPI platform, including all optional dependencies, the appropriate command is pip install fastapi[all]. Likewise, if you want to install and utilize the full-blown uvicorn server, you should run the pip install uvicorn command. Also, install the bcrypt module for encryption-related tasks.

At this point, you should have installed all the needed FastAPI module dependencies from the pydantic and starlette module components in your Python environment. Furthermore, the python-multipart module is required to create a REST API that handles form parameters. The installed uvicorn, however, is an ASGI-based server that will run your FastAPI applications. The Asynchronous Server Gateway Interface (ASGI) server that FastAPI uses makes it the fastest Python framework at the time of writing. The uvicorn server has the capability to run both synchronous and asynchronous services.

After the installation and configuration of the essential tools, modules, and IDE, let us now start our first API implementation using the framework.

 

Initializing and configuring FastAPI

Learning how to create applications using FastAPI is easy and straightforward. A simple application can be created just by creating a main.py file inside your /ch01 project folder. In our online academic discussion forum, for instance, the application started with this code:

from fastapi import FastAPI
app = FastAPI()

This initializes the FastAPI framework. The application needs to instantiate the core FastAPI class from the fastapi module and use app as the reference variable to the object. Then, this object is used later as a Python @app decorator, which provides our application with some features such as routes, middleware, exception handlers, and path operations.

Important note

You can replace app with your preferred but valid Python variable name, such as main_app, forum, or myapp.

Now, your application is ready to manage REST APIs that are technically Python functions. But to declare them as REST service methods, we need to decorate them with the appropriate HTTP request method provided by the path operation @app decorator. This decorator contains the get(), post(), delete(), put(), head(), patch(), trace(), and options() path operations, which correspond to the eight HTTP request methods. And these path operations are decorated or annotated on top of the Python functions that we want to handle the request and response.

In our specimen, the first sample that the REST API created was this:

@app.get("/ch01/index")
def index():
    return {"message": "Welcome FastAPI Nerds"} 

The preceding is a GET API service method that returns a JSON object. To locally run our application, we need to execute the following command:

uvicorn main:app --reload

This command will load the forum application to the uvicorn live server through the application’s main.py file with FastAPI object referencing. Live reload is allowed by adding the --reload option, which enables the restart of the development server whenever there are changes in the code.

Figure 1.1 – The uvicorn console log

Figure 1.1 – The uvicorn console log

Figure 1.1 shows that uvicorn uses localhost to run the application with the default port 8000. We can access our index page through http://localhost:8000/ch01/index. To stop the server, you just need to press the Ctrl + C keyboard keys.

After running our first endpoint, let us now explore how to implement the other types of HTTP methods, namely POST, DELETE, PUT, and PATCH.

 

Designing and implementing REST APIs

The Representation State Transfer (REST) API makes up the rules, processes, and tools that allow interaction among microservices. These are method services that are identified and executed through their endpoint URLs. Nowadays, focusing on API methods before building a whole application is one of the most popular and effective microservices design strategies. This approach, called an API-first microservices development, focuses first on the client’s needs and then later identifies what API service methods we need to implement for these client requirements.

In our online academic discussion forum app, software functionality such as user sign-up, login, profile management, message posting, and managing post replies are some of the crucial needs we prioritized. In a FastAPI framework, these features are implemented as services using functions that are defined using Python’s def keyword, with the association of the appropriate HTTP request method through the path operations provided by @app.

The login service, which requires username and password request parameters from the user, is implemented as a GET API method:

@app.get("/ch01/login/")
def login(username: str, password: str):
    if valid_users.get(username) == None:
        return {"message": "user does not exist"}
    else:
        user = valid_users.get(username)
        if checkpw(password.encode(), 
                   user.passphrase.encode()):
            return user
        else:
            return {"message": "invalid user"}

This login service uses bcrypt’s checkpw() function to check whether the password of the user is valid. Conversely, the sign-up service, which also requires user credentials from the client in the form of request parameters, is created as a POST API method:

@app.post("/ch01/login/signup")
def signup(uname: str, passwd: str):
    if (uname == None and passwd == None):
        return {"message": "invalid user"}
    elif not valid_users.get(uname) == None:
        return {"message": "user exists"}
    else:
        user = User(username=uname, password=passwd)
        pending_users[uname] = user
        return user

Among the profile management services, the following update_profile() service serves as a PUT API service, which requires the user to use an entirely new model object for profile information replacement and the client’s username to serve as the key:

@app.put("/ch01/account/profile/update/{username}")
def update_profile(username: str, id: UUID, 
                     new_profile: UserProfile):
    if valid_users.get(username) == None:
        return {"message": "user does not exist"}
    else:
        user = valid_users.get(username)
        if user.id == id:
            valid_profiles[username] = new_profile
            return {"message": "successfully updated"}
        else:
            return {"message": "user does not exist"}

Not all services that carry out updates are PUT API methods, such as the following update_profile_name() service, which only requires the user to submit a new first name, last name, and middle initial for partial replacement of a client’s profile. This HTTP request, which is handier and more lightweight than a full-blown PUT method, only requires a PATCH action:

@app.patch("/ch01/account/profile/update/names/{username}")
def update_profile_names(username: str, id: UUID, 
                          new_names: Dict[str, str]):
    if valid_users.get(username) == None:
        return {"message": "user does not exist"}
    elif new_names == None:
        return {"message": "new names are required"}
    else:
        user = valid_users.get(username)
        if user.id == id:
            profile = valid_profiles[username]
            profile.firstname = new_names['fname']
            profile.lastname = new_names['lname']
            profile.middle_initial = new_names['mi']
            valid_profiles[username] = profile
            return {"message": "successfully updated"}
        else:
            return {"message": "user does not exist"}

The last essential HTTP services that we included before building the application are the DELETE API methods. We use these services to delete records or information given a unique identification, such as username and a hashed id. An example is the following delete_post_discussion() service that allows a user to delete a posted discussion when given a username and the UUID (Universally Unique Identifier) of the posted message:

@app.delete("/ch01/discussion/posts/remove/{username}")
def delete_discussion(username: str, id: UUID):
    if valid_users.get(username) == None:
        return {"message": "user does not exist"}
    elif discussion_posts.get(id) == None:
        return {"message": "post does not exist"}
    else:
        del discussion_posts[id] 
        return {"message": "main post deleted"}

All path operations require a unique endpoint URL in the str format. A good practice is to start all URLs with the same top-level base path, such as /ch01, and then differ when reaching their respective subdirectories. After running the uvicorn server, we can check and validate whether all our URLs are valid and running by accessing the documentation URL, http://localhost:8000/docs. This path will show us a OpenAPI dashboard, as shown in Figure 1.2, listing all the API methods created for the application. Discussions on the OpenAPI will be covered in Chapter 9, Utilizing Other Advanced Features.

Figure 1.2 – A Swagger OpenAPI dashboard

Figure 1.2 – A Swagger OpenAPI dashboard

After creating the endpoint services, let us scrutinize how FastAPI manages its incoming request body and the outgoing response.

 

Managing user requests and server response

Clients can pass their request data to FastAPI endpoint URLs through path parameters, query parameters, or headers to pursue service transactions. There are standards and ways to use these parameters to obtain incoming requests. Depending on the goal of the services, we use these parameters to influence and build the necessary responses the clients need. But before we discuss these various parameter types, let us explore first how we use type hinting in FastAPI’s local parameter declaration.

Parameter type declaration

All request parameters are required to be type-declared in the method signature of the service method applying the PEP 484 standard called type hints. FastAPI supports common types such as None, bool, int, and float and container types such as list, tuple, dict, set, frozenset, and deque. Other complex Python types such as datetime.date, datetime.time, datetime.datetime, datetime.delta, UUID, bytes, and Decimal are also supported.

The framework also supports the data types included in Python’s typing module, responsible for type hints. These data types are standard notations for Python and variable type annotations that can help to pursue type checking and model validation during compilation, such as Optional, List, Dict, Set, Union, Tuple, FrozenSet, Iterable, and Deque.

Path parameters

FastAPI allows you to obtain request data from the endpoint URL of an API through a path parameter or path variable that makes the URL somewhat dynamic. This parameter holds a value that becomes part of a URL indicated by curly braces ({}). After setting off these path parameters within the URL, FastAPI requires these parameters to be declared by applying type hints.

The following delete_user() service is a DELETE API method that uses a username path parameter to search for a login record for deletion:

@app.delete("/ch01/login/remove/{username}")
def delete_user(username: str):
    if username == None:
    return {"message": "invalid user"}
else:
    del valid_users[username]
    return {"message": "deleted user"}

Multiple path parameters are acceptable if the leftmost variables are more likely to be filled with values than the rightmost variables. In other words, the importance of the leftmost path variables will make the process more relevant and correct than those on the right. This standard is applied to ensure that the endpoint URL will not look like other URLs, which might cause some conflicts and confusion. The following login_with_token() service follows this standard, since username is a primary key and is as strong as, or even stronger than, its next parameter, password. There is an assurance that the URL will always look unique every time the endpoint is accessed because username will always be required, as well as password:

@app.get("/ch01/login/{username}/{password}")
def login_with_token(username: str, password:str, 
                     id: UUID):
    if valid_users.get(username) == None:
        return {"message": "user does not exist"}
    else:
        user = valid_users[username]
        if user.id == id and checkpw(password.encode(), 
                 user.passphrase):
            return user
        else:
            return {"message": "invalid user"}

Unlike other web frameworks, FastAPI is not friendly with endpoint URLs that belong to base paths or top-level domain paths with different subdirectories. This occurrence happens when we have dynamic URL patterns that look the same as the other fixed endpoint URLs when assigned a specific path variable. These fixed URLs are implemented sequentially after these dynamic URLs. An example of these are the following services:

@app.get("/ch01/login/{username}/{password}")
def login_with_token(username: str, password:str, 
                     id: UUID):
    if valid_users.get(username) == None:
        return {"message": "user does not exist"}
    else:
        user = valid_users[username]
        if user.id == id and checkpw(password.encode(), 
                      user.passphrase.encode()):
            return user
        else:
            return {"message": "invalid user"}
@app.get("/ch01/login/details/info")
def login_info():
        return {"message": "username and password are 
                            needed"}

This will give us an HTTP Status Code 422 (Unprocessable Entity) when accessing http://localhost:8080/ch01/login/details/info. There should be no problem accessing the URL, since the API service is almost a stub or trivial JSON data. What happened in this scenario is that the fixed path’s details and info path directories were treated as username and password parameter values, respectively. Because of confusion, the built-in data validation of FastAPI will show us a JSON-formatted error message that says, {"detail":[{"loc":["query","id"],"msg":"field required","type":"value_error.missing"}]}. To fix this problem, all fixed paths should be declared first before the dynamic endpoint URLs with path parameters. Thus, the preceding login_info() service should be declared first before login_with_token().

Query parameters

A query parameter is a key–value pair supplied after the end of an endpoint URL, indicated by a question mark (?). Just like the path parameter, this also holds the request data. An API service can manage a series of query parameters separated by an ampersand (&). Like in path parameters, all query parameters are also declared in the service method. The following login service is a perfect specimen that uses query parameters:

@app.get("/ch01/login/")
def login(username: str, password: str):
    if valid_users.get(username) == None:
        return {"message": "user does not exist"}
    else:
        user = valid_users.get(username)
        if checkpw(password.encode(), 
               user.passphrase.encode()):
            return user
        else:
            return {"message": "invalid user"}

The login service method uses username and password as query parameters in the str types. Both are required parameters, and assigning them with None as parameter values will give a compiler error.

FastAPI supports query parameters that are complex types, such as list and dict. But these Python collection types cannot specify the type of objects to store unless we apply the generic type hints for Python collections. The following delete_users() and update_profile_names() APIs use generic type hints, List and Dict, in declaring query parameters that are container types with type checking and data validation:

from typing import Optional, List, Dict
@app.delete("/ch01/login/remove/all")
def delete_users(usernames: List[str]):
    for user in usernames:
        del valid_users[user]
    return {"message": "deleted users"}
@app.patch("/ch01/account/profile/update/names/{username}")
def update_profile_names(username: str, id: UUID, 
                         new_names: Dict[str, str]):
    if valid_users.get(username) == None:
        return {"message": "user does not exist"}
    elif new_names == None:
        return {"message": "new names are required"}
    else:
        user = valid_users.get(username)
        if user.id == id:
            profile = valid_profiles[username]
            profile.firstname = new_names['fname']
            profile.lastname = new_names['lname']
            profile.middle_initial = new_names['mi']
            valid_profiles[username] = profile
            return {"message": "successfully updated"}
        else:
            return {"message": "user does not exist"}

FastAPI also allows you to explicitly assign default values to service function parameters.

Default parameters

There are times that we need to specify default values to the query parameter(s) and path parameter(s) of some API services to avoid validation error messages such as field required and value_error.missing. Setting default values to parameters will allow the execution of an API method with or without supplying the parameter values. Depending on the requirement, assigned default values are usually 0 for numeric types, False for bool types, empty string for string types, an empty list ([]) for List types, and an empty dictionary ({}) for Dict types. The following delete pending users() and change_password() services show us how to apply default values to the query parameter(s) and path parameter(s):

@app.delete("/ch01/delete/users/pending")
def delete_pending_users(accounts: List[str] = []):
    for user in accounts:
        del pending_users[user]
    return {"message": "deleted pending users"}
@app.get("/ch01/login/password/change")
def change_password(username: str, old_passw: str = '',
                         new_passw: str = ''):
    passwd_len = 8
    if valid_users.get(username) == None:
        return {"message": "user does not exist"}
    elif old_passw == '' or new_passw == '':
        characters = ascii_lowercase
        temporary_passwd = 
             ''.join(random.choice(characters) for i in 
                     range(passwd_len))
        user = valid_users.get(username)
        user.password = temporary_passwd
        user.passphrase = 
                  hashpw(temporary_passwd.encode(),gensalt())
        return user
    else:
        user = valid_users.get(username)
        if user.password == old_passw:
            user.password = new_passw
            user.passphrase = hashpw(new_pass.encode(),gensalt())
            return user
        else:
            return {"message": "invalid user"}

delete_pending_users() can be executed even without passing any accounts argument, since accounts will be always an empty List by default. Likewise, change_password() can still continue its process without passing any old_passwd and new_passw, since they are both always defaulted to empty str. hashpw() is a bcrypt utility function that generates a hashed passphrase from an autogenerated salt.

Optional parameters

If the path and/or query parameter(s) of a service is/are not necessarily needed to be supplied by the user, meaning the API transactions can proceed with or without their inclusion in the request transaction, then we set them as optional. To declare an optional parameter, we need to import the Optional type from the typing module and then use it to set the parameter. It should wrap the supposed data type of the parameter using brackets ([]) and can have any default value if needed. Assigning the Optional parameter to a None value indicates that its exclusion from the parameter passing is allowed by the service, but it will hold a None value. The following services depict the use of optional parameters:

from typing import Optional, List, Dict
@app.post("/ch01/login/username/unlock")
def unlock_username(id: Optional[UUID] = None):
    if id == None:
        return {"message": "token needed"}
    else:
        for key, val in valid_users.items():
            if val.id == id:
                return {"username": val.username}
        return {"message": "user does not exist"}
@app.post("/ch01/login/password/unlock")
def unlock_password(username: Optional[str] = None, 
                    id: Optional[UUID] = None):
    if username == None:
        return {"message": "username is required"}
    elif valid_users.get(username) == None:
        return {"message": "user does not exist"}
    else:
        if id == None:
            return {"message": "token needed"}
        else:
            user = valid_users.get(username)
            if user.id == id:
                return {"password": user.password}
            else:
                return {"message": "invalid token"}

In the online academic discussion forum application, we have services such as the preceding unlock_username() and unlock_password() services that declare all their parameters as optional. Just do not forget to apply exception handling or defensive validation in your implementation when dealing with these kinds of parameters to avoid HTTP Status 500 (Internal Server Error).

Important note

The FastAPI framework does not allow you to directly assign the None value to a parameter just to declare an optional parameter. Although this is allowed with the old Python behavior, this is no longer recommended in the current Python versions for the purpose of built-in type checking and model validation.

Mixing all types of parameters

If you are planning to implement an API service method that declares optional, required, and default query and path parameters altogether, you can pursue it because the framework supports it, but approach it with some caution due to some standards and rules:

@app.patch("/ch01/account/profile/update/names/{username}")
def update_profile_names(id: UUID, username: str = '' , 
           new_names: Optional[Dict[str, str]] = None):
    if valid_users.get(username) == None:
        return {"message": "user does not exist"}
    elif new_names == None:
        return {"message": "new names are required"}
    else:
        user = valid_users.get(username)
        if user.id == id:
            profile = valid_profiles[username]
            profile.firstname = new_names['fname']
            profile.lastname = new_names['lname']
            profile.middle_initial = new_names['mi']
            valid_profiles[username] = profile
            return {"message": "successfully updated"}
        else:
            return {"message": "user does not exist"}

The updated version of the preceding update_profile_names() service declares a username path parameter, a UUID id query parameter, and an optional Dict[str, str] type. With mixed parameter types, all required parameters should be declared first, followed by default parameters, and last in the parameter list should be the optional types. Disregarding this ordering rule will generate a compiler error.

Request body

A request body is a body of data in bytes transmitted from a client to a server through a POST, PUT, DELETE, or PATCH HTTP method operation. In FastAPI, a service must declare a model object to represent and capture this request body to be processed for further results.

To implement a model class for the request body, you should first import the BaseModel class from the pydantic module. Then, create a subclass of it to utilize all the properties and behavior needed by the path operation in capturing the request body. Here are some of the data models used by our application:

from pydantic import BaseModel
class User(BaseModel):
    username: str
    password: str
class UserProfile(BaseModel):
    firstname: str
    lastname: str
    middle_initial: str
    age: Optional[int] = 0
    salary: Optional[int] = 0
    birthday: date
    user_type: UserType

The attributes of the model classes must be explicitly declared by applying type hints and utilizing the common and complex data types used in the parameter declaration. These attributes can also be set as required, default, and optional, just like in the parameters.

Moreover, the pydantic module allows the creation of nested models, even the deeply nested ones. A sample of these is shown here:

class ForumPost(BaseModel):
    id: UUID
    topic: Optional[str] = None
    message: str
    post_type: PostType
    date_posted: datetime
    username: str
class ForumDiscussion(BaseModel):
    id: UUID
    main_post: ForumPost
    replies: Optional[List[ForumPost]] = None
    author: UserProfile

As seen in the preceding code, we have a ForumPost model, which has a PostType model attribute, and ForumDiscussion, which has a List attribute of ForumPost, a ForumPost model attribute, and a UserProfile attribute. This kind of model blueprint is called a nested model approach.

After creating these model classes, you can now inject these objects into the services that are intended to capture the request body from the clients. The following services utilize our User and UserProfile model classes to manage the request body:

@app.post("/ch01/login/validate", response_model=ValidUser)
def approve_user(user: User):
    if not valid_users.get(user.username) == None:
        return ValidUser(id=None, username = None, 
             password = None, passphrase = None)
    else:
        valid_user = ValidUser(id=uuid1(), 
             username= user.username, 
             password  = user.password, 
             passphrase = hashpw(user.password.encode(),
                          gensalt()))
        valid_users[user.username] = valid_user
        del pending_users[user.username]
        return valid_user
@app.put("/ch01/account/profile/update/{username}")
def update_profile(username: str, id: UUID, 
                   new_profile: UserProfile):
    if valid_users.get(username) == None:
        return {"message": "user does not exist"}
    else:
        user = valid_users.get(username)
        if user.id == id:
            valid_profiles[username] = new_profile
            return {"message": "successfully updated"}
        else:
            return {"message": "user does not exist"}

Models can be declared required, with a default instance value, or optional in the service method, depending on the specification of the API. Missing or incorrect details such as invalid password or None values in the approve_user() service will emit the Status Code 500 (Internal Server Error). How FastAPI handles exceptions will be part of Chapter 2, Exploring the Core Features, discussions.

Important note

There are two essential points we need to emphasize when dealing with BaseModel class types. First, the pydantic module has a built-in JSON encoder that converts the JSON-formatted request body to the BaseModel object. So, there is no need create a custom converter to map the request body to the BaseModel model. Second, to instantiate a BaseModel class, all its required attributes must be initialized immediately through the constructor’s named parameters.

Request headers

In a request-response transaction, it is not only the parameters that are accessible by the REST API methods but also the information that describes the context of the client where the request originated. Some common request headers such as User-Agent, Host, Accept, Accept-Language, Accept-Encoding, Referer, and Connection usually appear with request parameters and values during request transactions.

To access a request header, import first the Header function from the fastapi module. Then, declare the variable that has the same name as the header in the method service as str types and initialize the variable by calling the Header(None) function. The None argument enables the Header() function to declare the variable optionally, which is a best practice. For hyphenated request header names, the hyphen (-) should be converted to an underscore (_); otherwise, the Python compiler will flag a syntax error message. It is the task of the Header() function to convert the underscore (_) to a hyphen (-) during request header processing.

Our online academic discussion forum application has a verify_headers() service that retrieves core request headers needed to verify a client’s access to the application:

from fastapi import Header
@app.get("/ch01/headers/verify")
def verify_headers(host: Optional[str] = Header(None), 
                   accept: Optional[str] = Header(None),
                   accept_language: 
                       Optional[str] = Header(None),
                   accept_encoding: 
                       Optional[str] = Header(None),
                   user_agent: 
                       Optional[str] = Header(None)):
    request_headers["Host"] = host
    request_headers["Accept"] = accept
    request_headers["Accept-Language"] = accept_language
    request_headers["Accept-Encoding"] = accept_encoding
    request_headers["User-Agent"] = user_agent
    return request_headers

Important note

Non-inclusion of the Header() function call in the declaration will let FastAPI treat the variables as query parameters. Be cautious also with the spelling of the local parameter names, since they are the request header names per se except for the underscore.

Response data

All API services in FastAPI should return JSON data, or it will be invalid and may return None by default. These responses can be formed using dict, BaseModel, or JSONResponse objects. Discussions on JSONResponse will be discussed in the succeeding chapters.

The pydantic module’s built-in JSON converter will manage the conversion of these custom responses to a JSON object, so there is no need to create a custom JSON encoder:

@app.post("/ch01/discussion/posts/add/{username}")
def post_discussion(username: str, post: Post, 
                    post_type: PostType):
    if valid_users.get(username) == None:
        return {"message": "user does not exist"}
    elif not (discussion_posts.get(id) == None):
        return {"message": "post already exists"}
    else:
        forum_post = ForumPost(id=uuid1(), 
          topic=post.topic, message=post.message, 
          post_type=post_type, 
          date_posted=post.date_posted, username=username)
        user = valid_profiles[username]
        forum = ForumDiscussion(id=uuid1(), 
         main_post=forum_post, author=user, replies=list())
        discussion_posts[forum.id] = forum
        return forum

The preceding post_discussion() service returns two different hardcoded dict objects, with message as the key and an instantiated ForumDiscussion model.

On the other hand, this framework allows us to specify the return type of a service method. The setting of the return type happens in the response_model attribute of any of the @app path operations. Unfortunately, the parameter only recognizes BaseModel class types:

@app.post("/ch01/login/validate", response_model=ValidUser)
def approve_user(user: User):
    
    if not valid_users.get(user.username) == None:
        return ValidUser(id=None, username = None, 
                   password = None, passphrase = None)
    else:
        valid_user = ValidUser(id=uuid1(), 
         username= user.username, password = user.password,
          passphrase = hashpw(user.password.encode(),
                 gensalt()))
        valid_users[user.username] = valid_user
        del pending_users[user.username]
        return valid_user

The preceding approve_user() service specifies the required return of the API method, which is ValidUser.

Now, let us explore how FastAPI handles form parameters.

 

Handling form parameters

When API methods are designed to handle web forms, the services involved are required to retrieve form parameters instead of the request body because this form data is normally encoded as an application/x-www-form-urlencoded media type. These form parameters are conventionally string types, but the pydantic module’s JSON encoder can convert each parameter value to its respective valid type.

All the form parameter variables can be declared required, with default values, or optional using the same set of Python types we used previously. Then, the fastapi module has a Form function that needs to be imported to initialize these form parameter variables during their declaration. To set these form parameters as required, the Form() function must have the ellipses () argument, thus calling it as Form(…):

from fastapi import FastAPI, Form
@app.post("/ch01/account/profile/add", 
                        response_model=UserProfile)
def add_profile(uname: str, 
                fname: str = Form(...), 
                lname: str = Form(...),
                mid_init: str = Form(...),
                user_age: int = Form(...),
                sal: float = Form(...),
                bday: str = Form(...),
                utype: UserType = Form(...)):
    if valid_users.get(uname) == None:
        return UserProfile(firstname=None, lastname=None, 
              middle_initial=None, age=None, 
              birthday=None, salary=None, user_type=None)
    else:
        profile = UserProfile(firstname=fname, 
             lastname=lname, middle_initial=mid_init, 
             age=user_age, birthday=datetime.strptime(bday,
                '%m/%d/%Y'), salary=sal, user_type=utype)
        valid_profiles[uname] = profile
        return profile

The preceding add_profile() service shows us how to call the Form(…) function to return a Form object during the parameter declaration.

Important note

Form-handling services will not work if the python-multipart module is not installed.

Sometimes, we need browser cookies to establish an identity for our application, leave trails in the browser for every user transaction, or store product information for a purpose. If FastAPI can manage form data, it can also do the same with cookies.

 

Managing cookies

A cookie is a piece of information stored in the browser to pursue some purpose, such as login user authorization, web agent response generation, and session handling-related tasks. One cookie is always a key-value pair that are both string types.

FastAPI allows services to create cookies individually through the Response library class from its fastapi module. To use it, it needs to appear as the first local parameter of the service, but we do not let the application or client pass an argument to it. Using the dependency injection principle, the framework will provide the Response instance to the service and not the application. When the service has other parameters to declare, the additional declaration should happen right after the declaration of the Response parameter.

The Response object has a set_cookie() method that contains two required named parameters: the key, which sets the cookie name, and the value, which stores the cookie value. This method only generates one cookie and stores it in the browser afterward:

@app.post("/ch01/login/rememberme/create/")
def create_cookies(resp: Response, id: UUID, 
                   username: str = ''):
    resp.set_cookie(key="userkey", value=username)
    resp.set_cookie(key="identity", value=str(id))
    return {"message": "remember-me tokens created"}

The preceding create_cookies() method shows us the creation of remember-me tokens such as userkey and identity for the remember-me authorization of our online academic discussion forum project.

To retrieve these cookies, local parameters that have the same name as the cookies are declared in the service method as str types, since cookie values are always strings. As with Header and Form, the fastapi module also provides a Cookie function that is needed to initialize each declared cookie parameter variable. The Cookie() function should always have the None argument to set the parameters optionally, ensuring that the API method executes without problems whenever the headers are not present in the request transaction. The following access_cookie() service retrieves all the remember-me authorization cookies created by the previous service:

@app.get("/ch01/login/cookies")
def access_cookie(userkey: Optional[str] = Cookie(None), 
           identity: Optional[str] = Cookie(None)):
    cookies["userkey"] = userkey
    cookies["identity"] = identity
    return cookies
 

Summary

This chapter is essential to familiarize ourselves with FastAPI and understand its basic components. The concept that we can get from this chapter can measure how much adjustment and effort we need to invest into translating or rewriting some existing applications to FastAPI. Knowing its basics will help us learn how to install its modules, structure the project directories, and learn the core library classes and functions needed to build a simple enterprise-grade application.

With the help of our recipe online academic discussion forum application, this chapter showed us how to build different REST APIs associated with HTTP methods using the FastAPI module class and Python def functions. From there, we learned how to capture incoming request data and headers using the local parameters of the API methods and how these API methods should return a response to the client. And through this chapter, we saw how easy it is for FastAPI to capture form data from <form></form> of any UI templates and that is using the Form function. Aside from the Form function, the FastAPI module also has the Cookie function to help us create and retrieve cookies from the browser, and Header to retrieve the request header part of an incoming request transaction.

Overall, this chapter has prepared us for advanced discussions that will center on other features of FastAPI that can help us upgrade our simple applications to full-blown ones. The next chapter will cover these essential core features, which will provide our application with the needed response encoder and generator, exception handlers, middleware, and other components related to asynchronous transactions.

About the Author
  • Sherwin John C. Tragura

    Sherwin John C. Tragura, an alumnus of the University of the Philippines Los Banos, is a Technical Consultant and Subject Matter Expert on Java, Python, and .NET web and microservice applications. He has been a Java and C# developer since 2005 and a hybrid mobile and Python developer since 2015. He is also a Java/C#/Python full-stack technical trainer and consultant for several companies in Manila and Cebu. Moreover, he collaborated with Packt to successfully published the books Spring MVC Blueprints, Spring 5 Cookbook, and Building Python Microservices with FastAPI. He also authored the video reference entitled Modern Java Web Applications with Spring Boot 2.x, published by Packt.

    Browse publications by this author
Latest Reviews (1 reviews total)
Building Python Microservices with FastAPI
Unlock this book and the full library FREE for 7 days
Start now