Lathkar High Performance Web Apps With FastAPI RuLit Me 794908
Lathkar High Performance Web Apps With FastAPI RuLit Me 794908
Malhar Lathkar
High-Performance Web Apps with FastAPI: The Asynchronous Web
Framework Based on Modern Python
Malhar Lathkar
Nanded, Maharashtra, India
Acknowledgments����������������������������������������������������������������������������xvii
Introduction���������������������������������������������������������������������������������������xix
v
Table of Contents
Pydantic���������������������������������������������������������������������������������������������������������23
Uvicorn����������������������������������������������������������������������������������������������������������23
Installation of FastAPI�����������������������������������������������������������������������������������������24
Summary������������������������������������������������������������������������������������������������������������28
vi
Table of Contents
Chapter 4: Templates��������������������������������������������������������������������������93
HTML Response��������������������������������������������������������������������������������������������������93
Template Engine�������������������������������������������������������������������������������������������������96
Hello World Template������������������������������������������������������������������������������������������98
Template with Path Parameter��������������������������������������������������������������������������100
Template Variables��������������������������������������������������������������������������������������������101
Passing dict in Template Context�����������������������������������������������������������������102
Conditional Blocks in Template��������������������������������������������������������������������103
Loop in Template�����������������������������������������������������������������������������������������105
Serving Static Assets����������������������������������������������������������������������������������������107
Using JavaScript in Template����������������������������������������������������������������������108
Static Image������������������������������������������������������������������������������������������������111
CSS As a Static Asset����������������������������������������������������������������������������������113
vii
Table of Contents
Chapter 5: Response�������������������������������������������������������������������������121
Response Model������������������������������������������������������������������������������������������������122
Cookies�������������������������������������������������������������������������������������������������������������125
set_cookie() Method������������������������������������������������������������������������������������125
Cookie Parameter����������������������������������������������������������������������������������������126
Headers�������������������������������������������������������������������������������������������������������������129
Header Parameter���������������������������������������������������������������������������������������130
Response Status Code��������������������������������������������������������������������������������������131
Response Types������������������������������������������������������������������������������������������������134
HTMLResponse��������������������������������������������������������������������������������������������135
JSONResponse��������������������������������������������������������������������������������������������136
StreamingResponse������������������������������������������������������������������������������������136
FileResponse�����������������������������������������������������������������������������������������������138
RedirectResponse����������������������������������������������������������������������������������������139
Summary����������������������������������������������������������������������������������������������������������141
viii
Table of Contents
aiosqlite Module������������������������������������������������������������������������������������������������154
SQLAlchemy������������������������������������������������������������������������������������������������������156
async in SQLAlchemy����������������������������������������������������������������������������������������163
databases Module���������������������������������������������������������������������������������������164
Core Expression Language��������������������������������������������������������������������������165
Table Class Methods������������������������������������������������������������������������������������166
FastAPI Path Operations������������������������������������������������������������������������������167
PyMongo for MongoDB�������������������������������������������������������������������������������������170
Motor for MongoDB�������������������������������������������������������������������������������������������177
Summary����������������������������������������������������������������������������������������������������������179
ix
Table of Contents
x
Table of Contents
Testing��������������������������������������������������������������������������������������������������������������260
Testing WebSocket��������������������������������������������������������������������������������������263
Testing Databases���������������������������������������������������������������������������������������265
AsyncClient�������������������������������������������������������������������������������������������������������270
Summary����������������������������������������������������������������������������������������������������������272
Index�������������������������������������������������������������������������������������������������295
xi
About the Author
Malhar Lathkar is an independent developer,
trainer, technical writer, and author with
over 30 years of experience. He holds a
postgraduate degree in electronics. After
a brief stint as a degree college lecturer,
he entered into the software training and
development field as an entrepreneur.
Malhar is mostly a self-taught professional.
Over the years, he has gained proficiency
in various programming technologies and
guided thousands of students and professionals from India and different
countries around the world. Malhar also offers software training services to
corporates.
He has been associated with many EdTech companies as a freelance
content developer and subject matter expert. He has also written a few
books that have been published by well-known publishing houses.
Malhar is frequently invited to conduct workshops, deliver technical
talks for the students in various engineering colleges, and work as a jury to
evaluate student projects for hackathon competitions.
He enjoys Indian classical music. Being an avid sportsman during
college days, he keeps a keen eye on all the sporting action around
the world.
xiii
About the Technical Reviewer
Jeff Chiu is a senior software engineer with
over ten years of experience working on
Django, Python, and REST APIs. He has
worked as senior engineer at several major
Silicon Valley tech companies building
platform infrastructure. Jeff writes clean,
consistent code. Outside of work, he mentors
other aspiring engineers and early career
professionals through the online community.
He enjoys this so much that he has built multiple apps and created
discussion forums to help engineers receive constructive feedback.
His work portfolio can be found at https://jeffchiucp.github.io/
portfolio/.
xv
Acknowledgments
At the outset, I express my sincere gratitude toward Apress (Springer
Nature) Publications for giving me this opportunity to write this book and
be a part of the Apress family. I thank the editorial team and especially Jeff
Chiu – the technical reviewer – for his invaluable inputs while finalizing
the draft of this book.
I would also like to acknowledge the graphics designers who have
produced a splendid cover page for this book.
The unerring and unconditional support of my family (my wife
Jayashree, daughter Sukhada, and son-in-law Shripad) in my endeavors
has always been my biggest strength. They have stood by me in good and
bad times. A very dear friend Dr. Kishore Atnurkar and his wife Seema,
who are no less than a part of my family, have been appreciative of my
work and have always given me a lot of encouragement. It wouldn’t be out
of place to acknowledge their contribution.
Throughout my academic life, I have been blessed with guidance from
some highly inspiring teachers. Their profound influence has made me a
lifelong learner. I hereby pay my respectful regards to all my teachers.
You always learn more when you teach. I would like to thank
thousands of my students for being a part of my learning journey.
Finally, for all those who have been involved in bringing out this book,
a big thank you!
xvii
Introduction
As a programming language, Python has been continuously evolving. New
features and capabilities are incorporated with each version of Python.
This has made Python the preferred choice of developers working in
different application domains such as machine learning, GUI construction,
API development, etc.
With the inclusion of support for asynchronous processing, using
Python in building high-performance web apps has become increasingly
prevalent. FastAPI is one of the fastest web application frameworks.
It implements the ASGI (Asynchronous Server Gateway Interface)
specification.
FastAPI is a relatively young framework. Yet it has become quite
popular with the developer community. This book aims to help the reader
get acquainted with its salient features. Experienced Python developers
looking to leverage the flexibility of Python and the powerful features
introduced in modern Python as well as computer science engineering
students at graduate and postgraduate levels will also benefit immensely
from the practical approach adapted in the book.
xix
Introduction
xx
CHAPTER 1
Introduction
to FastAPI
The recent surge in the popularity of Python as a programming language
is mainly due to its libraries used in the field of data science applications.
However, Python is also extensively used for web application development,
thanks to the abundance of its web application frameworks.
FastAPI is the latest entrant in the long list of Python’s web application
frameworks. However, it’s not just another framework as it presents some
distinct advantages over the others. Considered to be one of the “fastest,”
FastAPI leverages the capabilities of modern Python. In this chapter,
we shall get acquainted with the important features on top of which the
FastAPI library is built.
This chapter covers the following topics:
• Type hints
• Asynchronous processing
• REST architecture
• HTTP verbs
• FastAPI dependencies
• FastAPI installation
Type Hints
Python is a dynamically typed language. In contrast, the languages
C/C++ and Java are statically typed, wherein the type of the variable must
be declared before assigning a value to it. During the lifetime of a C/C++/
Java program, a variable can hold the data of its declared type only. In
Python, it is the other way round. The type of the variable is decided by
the value assigned to it. It may change dynamically on each assignment.
The interaction in the Python console in Listing 1-1 shows Python’s
dynamic typing.
>>> x=10
>>> #x is an int variable
>>> x=(1,2,3)
>>> type(x)
<class 'tuple'>
>>> x=[1,2,3]
>>> #x is now a list
>>> type(x)
<class 'list'>
>>> x="Hello World"
>>> #x now becomes a str variable
>>> type(x)
<class 'str'>
2
Chapter 1 Introduction to FastAPI
#hint.py
def division(num, den):
return num/den
Let us import this function and call it from the Python prompt as
shown in Listing 1-3.
The first two calls to the division() function are successful, but the
TypeError exception is thrown for the third case, because the division
operation of a numeric and a nonnumeric operand fails.
The default Python interpreter (Python shell) that comes with the
standard installation of Python is rather rudimentary in nature. The more
advanced Python runtimes such as IPython and Jupyter Notebook as well
as many Python IDEs like VS Code, PyCharm (including IDLE – a basic
IDE shipped with the standard library) are more intelligent, having useful
features such as autocompletion, syntax highlighting, and type ahead help.
Figure 1-1 shows the autocompletion feature of IPython.
3
Chapter 1 Introduction to FastAPI
Although the Python interpreter still doesn’t enforce type checking, the
annotation of parameters with data types is picked by Python IDEs. The
IDE lets the user know what types of values are to be passed as arguments
to the function while calling.
Let us see how VS Code – a very popular IDE for program development
not only in Python but in many other languages too – reacts to the type
hints. In Figure 1-2, we find that the preceding function is defined and its
return value is displayed.
4
Chapter 1 Introduction to FastAPI
All the standard types, those defined in imported modules, and user-
defined types can be used as hints (Listing 1-5). Type hints can be used
for global variables, function and method parameters, as well as local
variables inside them.
5
Chapter 1 Introduction to FastAPI
#hintexample.py
arg1: int = int(input("Enter a number.."))
arg2: int = int(input("Enter a number.."))
6
Chapter 1 Introduction to FastAPI
>>> l2.append("hello")
>>> l2
[100, 25.5, 0.022, 'hello']
The typing module also defines Union, Any, and Optional types. The
Union type should be used as a hint to provide a list of possible data types
for an object. Consider the case in Listing 1-8 where each item in this list is
expected to be of either int or str type.
You can of course assign any other value; the static type checker linters
will detect the mismatch.
7
Chapter 1 Introduction to FastAPI
Asynchronous Processing
Python supports concurrent processing by using a multithreading
approach ever since its earlier versions. The asynchronous processing
support has been added to Python from the Python 3.5 version onward.
In a multithreaded application, many individual threads of operation are
there in the program, and the CPU coordinates their concurrent execution.
On the other hand, in the asynchronous approach, only a single thread
runs, but it has the ability to switch between functions.
Whenever an asynchronous function reaches an event or condition,
it voluntarily yields to another function. By the time the result from the
other function is obtained, the original function can attend some other
operations. In this way, more than one process in an application can run
concurrently, without intervention from the operating system. Moreover,
as there’s a single running thread, it doesn’t involve heavy processor
resources. Asynchronous processing is also sometimes (and appropriately)
called cooperative multitasking.
8
Chapter 1 Introduction to FastAPI
>>> hello()
Hello World
>>> sayhello()
<coroutine object sayhello at 0x0000015CC0295DB0>
9
Chapter 1 Introduction to FastAPI
coroutine prints the iteration number, pauses for two seconds, and goes
back to main(). The outer coroutine then prints the number and goes for
the next iteration.
#coroutines.py
import asyncio
import time
async def main():
for i in range(1,6):
await myfunction(i)
print ('In main', i)
asyncio.run(main())
Go ahead and execute this Python script. The output shows how the
two coroutines take turns concurrently:
In myfunction 1
In main 1
In myfunction 2
In main 2
In myfunction 3
In main 3
In myfunction 4
In main 4
In myfunction 5
In main 5
10
Chapter 1 Introduction to FastAPI
ASGI
Many of the well-known web application frameworks of Python
(like Django and Flask) were developed before the introduction of
asynchronous capabilities. These frameworks implement WSGI (Web
Server Gateway Interface) specifications. The request-response cycle
between the client and the server is synchronous in nature and hence not
particularly efficient.
To leverage the functionality of the asyncio module, new specifications
for web servers, frameworks, and applications have been devised in the
form of ASGI, which stands for Asynchronous Server Gateway Interface.
An ASGI callable object is in fact a coroutine having three parameters:
Listing 1-12 shows a very simple ASGI callable coroutine. It calls the
send coroutine as an awaitable object to insert the HTTP headers and the
body in the client’s response. These calls are nonblocking in nature so that
the server can engage with many clients concurrently.
11
Chapter 1 Introduction to FastAPI
],
})
await send({
'type': 'http.response.body',
'body': b'Hello, world!',
})
# main.py
import uvicorn
await send({
'type': 'http.response.body',
'body': b'Hello, world!',
})
12
Chapter 1 Introduction to FastAPI
if __name__ == "__main__":
uvicorn.run("main:app", port=5000, log_level="info")
Run this program from the command line (make sure that the Uvicorn
package is installed before running). The Uvicorn server starts serving
the applications and is waiting for incoming requests from a client at port
5000 of the localhost. Launch your favorite browser and visit the http://
localhost:5000 URL. The browser window shows (Figure 1-3) the Hello
World message as the response.
13
Chapter 1 Introduction to FastAPI
What Is an API?
The term API is very frequently used within the web developer
community. It is an acronym for Application Programming Interface. The
word interface generally refers to a common meeting ground between
two isolated and independent environments. The interaction between
them takes place as per a predefined set of rules and protocols. To give a
simplistic analogy, the waiter in a restaurant acts as an interface between
the customer and the kitchen, accepting the order for the items in the
menu, passing on the order to the kitchen staff for preparing the dish, and
in turn serving it to the customer. Figure 1-4 depicts the analogy between
the roles of a waiter and an API.
14
Chapter 1 Introduction to FastAPI
can of course visit the company’s website, go through the registration and
authentication process, and order the food of their choice. Now you have
a food delivery service (such as Grubhub in the United States, Swiggy in
India) that wants to enable its clients to use the order booking system.
Obviously, an access by an unauthorized user will be denied. (Or else, the
situation will be akin to each customer in the restaurant directly going to
the kitchen and ordering the chef to prepare a certain dish that is not even
on the menu!)
This is where the API comes into play. The company will deploy a
system wherein the delivery agency website will be authorized and asked
to place the order in a specified format. The request received will then be
processed appropriately by the company’s application, and the result will
also be sent to the agency in a predefined format for further consumption.
This mechanism that facilitates communication between two different
applications (here, the company’s order processing application and the
delivery agency’s application) by following certain streamlined formats
and protocols is what makes an API. Please refer to Figure 1-5 for better
understanding.
15
Chapter 1 Introduction to FastAPI
16
Chapter 1 Introduction to FastAPI
REST
REST (short for REpresentational State Transfer) is a software
architectural style that defines how a web application should behave. The
term REST was first introduced by Roy Fielding in the year 2000. A web
API that follows this architectural style is often called the RESTful API.
Unlike RPC or SOAP, REST is not a protocol. Moreover, REST is a
resource-based architecture rather than action based as is the case in
RPC or SOAP. Everything on the REST server is a resource, may it be a
file, an image, or a row in a table of a database. The REST API provides
a controlled access to its resources so that the client can retrieve and
modify them.
REST Constraints
A web API based on the REST architecture must follow certain design
principles, also called constraints, laid down by Roy Fielding in his
research paper. The following are the six REST constraints.
Uniform interface: This constraint essentially means that the request for
a certain resource on the server from any client must be the same. In other
words, a resource on the server must have a Uniform Resource Identifier
(URI), and the request for a resource should be complete in the sense it
should contain all the information required for retrieving and processing it.
Statelessness: When the server receives any request for a certain
resource from a client, it should be processed entirely in isolation
without any context of any previous transaction. Any change in the state
of the concerned resource is communicated back to the client. REST is
implemented over HTTP, and the HTTP server doesn’t retain the session
information, thereby increasing the performance by removing server load.
Client–server: The client-server model is also the backbone of HTTP. It
ensures that there is a separation of concerns, leading to the evolution of
server and client applications independent of each other. As long as the
17
Chapter 1 Introduction to FastAPI
interface between them is not altered, servers and clients may also be
replaced and developed independently. In fact, the client is expected to
know only the URIs of the resources on the client and nothing else.
Cacheability: The term caching refers to storing the transient data in a
high-speed buffer so that subsequent access to it is faster. By marking the
API response as cacheable, it improves the performance of the client, as
well as the scalability of the server.
Layered system: Just as the server and client components are isolated
from each other (thanks to the client-server constraint of REST), the server
functionality itself can be further composed into multiple hierarchical
layers, each independent of the other. A certain layer designed to perform
a specific task can interact with only the immediate layer and none other.
This approach improves system scalability and enables the load balancing
of services across multiple networks and processors.
Code on demand: On most occasions, the server’s response is in either
the HTML or XML representation of the resource. However, according
to this constraint (which by the way is the only optional feature among
the six), the response may return some front-end executable component
such as JavaScript. The client can download the same. Though this feature
improves the extensibility, it can also be a potential security issue, hence
rarely implemented.
In a nutshell, implementing the preceding principles (constraints) has
the following advantages:
• Scalability
• Simplicity
• Modifiability
• Reliability
• Portability
• Visibility
18
Chapter 1 Introduction to FastAPI
HTTP Verbs
The principle of uniform interface says that the request message must
be self-sufficient and that it should contain everything that is required to
process the request. It should include the URI of the resource, the action
to be taken on it, and some additional data if required for the action to
be completed. The action part in the request is represented by HTTP
verbs or methods. The most used HTTP methods are POST, GET, PUT,
and DELETE. These methods correspond to CREATE, READ, UPDATE,
and DELETE operations on the server’s resource. These operations are
popularly known by the abbreviated form CRUD.
In addition to the abovementioned methods, the HTTP request
type can be of few other types (those being PATCH, HEAD, OPTIONS,
etc.). However, they are less prevalent, especially in the context of REST
architecture.
So, we can draw the inference that the client sends an HTTP request of
either POST, GET, PUT, or DELETE type to perform a CRUD operation on a
certain resource residing with the server. This request is intercepted by the
REST API designed as per the constraints explained earlier and forwarded
to the server for processing. In turn, the response is returned to the client
for its further consumption.
POST Method
The POST verb in the HTTP request indicates that a new resource is
intended to be created on the server. It corresponds to the CREATE
operation in the CRUD term. Since the creation of a new resource needs
certain data, it is included in the request as a data header. If the request is
successful, the HTTP response returns a Location header with a link to the
newly created resource with the 201 HTTP status. In case the operation is
not successful, the status code is either 200 (OK) or 204 (No Content).
19
Chapter 1 Introduction to FastAPI
Since invoking two identical POST requests will result in two different
resources containing the same information, it is not considered as
idempotent.
Examples of a POST request:
GET Method
The READ part in the CRUD operation is carried out by sending the HTTP
request with a GET method. The purpose of the GET operation is to retrieve
an existing resource on the server and return its XML/JSON representation
as the response. Success inserts the 200 (OK) status code in the response,
whereas its value is 404 (Not Found) or 400 (Bad Request) in case of failure.
The GET operation is risk-free in the sense that it only retrieves the
resource and doesn’t modify it. Furthermore, it is considered to be an
idempotent operation, as two identical GET requests return identical
responses.
Examples of a GET request:
PUT Method
The PUT method is used mainly to update an existing resource
(corresponding to the UPDATE part in CRUD). Like the POST operation, the
data required for update should be included in the request. Success returns
a 200 (OK) status code, and failure returns a 404 (Not Found) status code.
In some cases, the PUT operation may create a new resource,
depending on how the business logic of the API is written. In such a case,
the response contains a 201 status code. The response of the PUT method
generally doesn’t contain the representation of the newly created resource.
20
Chapter 1 Introduction to FastAPI
The difference between the POST and PUT is that POST requests are
made on resource collections, whereas PUT requests are made on a single
resource.
Examples of a PUT request:
DELETE Method
As the name suggests, the DELETE method is used to delete one or more
resources on the server. On successful execution, an HTTP response code
200 (OK) is sent. It may also be 202 (Accepted) if the action has been
queued or 204 (No Content) if the action has been performed but the
response does not include an entity.
DELETE operations are idempotent, like the GET method. If a resource
is deleted, it’s removed from the collection of resources. However, calling
DELETE on a resource a second time will return a 404 (Not Found) since it
was already removed.
Examples of a DELETE request:
FastAPI Dependencies
FastAPI is an asynchronous web framework that incorporates Python’s
type annotation feature. It is built on top of Starlette – Python’s ASGI
toolkit. FastAPI also uses Pydantic as an important building block for
validating the data models. An ASGI application should be hosted on an
asynchronous server; the fastest one around is Uvicorn.
21
Chapter 1 Introduction to FastAPI
Starlette
Starlette is a lightweight ASGI-compliant web framework, ideal for
leveraging the advantage of asyncio in building high-performance APIs
and web apps. Its simple and intuitive design makes it easily extensible.
Some of the important features of Starlette include the following.
WebSocket support: WebSocket is the extension of the HTTP protocol
to provide bidirectional, full-duplex communication between the client
and the server. WebSockets are used for building real-time applications.
Event handlers: Starlette supports intercepting and processing startup
and shutdown events in a web application. They can be effectively used
to handle certain initialization and mopping up tasks such as establishing
and closing the database connection as the application starts and closes.
22
Chapter 1 Introduction to FastAPI
Pydantic
Pydantic is another library that FastAPI uses as an important pillar. It is
an excellent Python library for data validation and parsing. FastAPI uses
it to describe the data part of a web application. It uses Python’s typing
annotation feature, enforcing type hints at runtime. Pydantic maps the
data to a Python class.
Using Pydantic data models makes it very easy to interact with
relational database ORMs (like SQLAlchemy) and ODMs (like Motor for
MongoDB); it takes care of the data validation aspect very efficiently.
Pydantic is capable of validating Python’s built-in types, user-defined
types, the types defined in the typing module (such as List and Dict), as
well as complex data types involving Pydantic’s recursive models.
We shall discuss Pydantic models in more detail in one of the
subsequent chapters.
Uvicorn
As mentioned earlier, Uvicorn is an asynchronous web server with high
performance. It has been designed as per the ASGI specifications defined
in the asgiref package. This package has an asgiref.server class which
is used as the base.
To enable WebSocket support, a standard version of Uvicorn needs
to be installed. It brings in Cython-based dependencies – uvloop and
httptools. The uvloop library provides a more efficient replacement for
the event loop defined in the asyncio module. The httptools on the other
hand is used to handle the HTTP protocol.
23
Chapter 1 Introduction to FastAPI
Installation of FastAPI
Now that we have understood the important prerequisites, it is now time
to introduce the FastAPI web application framework. FastAPI is a web
application framework based on Python’s modern features such as type
hints and support for asynchronous processing. The async feature makes it
extremely “fast” as compared with the other Python web frameworks.
FastAPI was developed by Sebastian Ramirez in December 2018.
FastAPI 0.79.0 is the currently available version (released in July 2022). In
spite of being very young, it has very quickly climbed up on the popularity
charts and is one of the most loved web frameworks.
So, let’s go ahead and install FastAPI (preferably in a virtual
environment). It’s easy. Just use the PIP utility to get it from the PyPI
repository. Ensure that you are using Python’s 3.6 version or later:
24
Chapter 1 Introduction to FastAPI
To get the list of packages installed, use the freeze subcommand of the
PIP utility (Listing 1-14).
pip3 freeze
anyio==3.6.1
click==8.1.3
colorama==0.4.5
fastapi==0.79.0
h11==0.13.0
httptools==0.4.0
idna==3.3
pydantic==1.9.1
python-dotenv==0.20.0
PyYAML==6.0
sniffio==1.2.0
starlette==0.19.1
typing_extensions==4.3.0
uvicorn==0.18.2
watchfiles==0.16.0
websockets==10.3
25
Chapter 1 Introduction to FastAPI
#main.py
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def index():
return {"message": "Hello World"}
26
Chapter 1 Introduction to FastAPI
The terminal shows the following log, effectively telling that the
application is being served at port 8000 of the localhost:
27
Chapter 1 Introduction to FastAPI
Summary
This chapter set the ball rolling for our journey of learning how to build
APIs with the FastAPI framework. In this chapter, we learned about
the modern concepts of Python (typing and asyncio modules), which
are very crucial in using FastAPI. We also took a quick revision of the
principles of REST architecture. After introducing the dependency libraries
of FastAPI – namely, Starlette, Pydantic, and Uvicorn – we actually installed
it and successfully executed a Hello World application.
In the next chapter, we shall discuss in detail the elements of a FastAPI
application such as the path operations, decorators, and views. You will
also understand how interactive API docs are generated and how to pass
path and query parameters to the view function.
28
CHAPTER 2
Getting Started
with FastAPI
The previous chapter laid the groundwork for exploring the powerful
features of the FastAPI web framework. We now know enough about the
type hinting and asynchronous processing mechanism that is extensively
implemented in FastAPI. It is primarily a tool for developing web APIs.
Hence, a brief discussion on the principles of REST architecture was also
given in the previous chapter.
This chapter helps you take the first steps toward building APIs. We
shall also learn about the API docs and parameters.
We shall discuss the following topics:
• Hello World
• Interactive API docs
• Path parameters
• Query parameters
• Validation of parameters
Hello World
Toward the end of the previous chapter, you saw a small FastAPI script that
renders a Hello World message in the browser. Let us revisit that script and
understand how it works in detail.
Conventionally, the first program written whenever you learn a new
programming language or a framework is the one that displays a “Hello
World” message. The objective is to verify that the respective environment
is correctly installed. It also helps in understanding the basic building
blocks of the application.
This app object is the main point of interaction between the ASGI
server and the client, as it is responsible for routing all the incoming
requests to the appropriate handlers and providing appropriate responses.
31
Chapter 2 Getting Started with FastAPI
@app.get("/")
async def index():
return {"message": "Hello World"}
What does the preceding code segment do? Whenever the application
object finds that the client has requested the “/” path with a GET request,
the index() function defined just below should be called. In other words,
the URL path “/” is mapped with the index() function, called the path
operation function. It usually returns a dict object. Its JSON form is
returned as the response to the client.
The Hello World application code is reproduced here for convenience
(Listing 2-2).
#main.py
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def index():
return {"message": "Hello World"}
Start Uvicorn
As mentioned before, throughout this book, we are going to use the Uvicorn
server to run a FastAPI application. It is an ASGI implementation for Python.
You can start the Uvicorn server either by using the command-line interface
of the Uvicorn library or programmatically by calling its run() function.
32
Chapter 2 Getting Started with FastAPI
To start the server from the command line, enter the following
statement in the command terminal of your OS:
import uvicorn
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def index():
return {"message": "Hello World"}
if __name__ == "__main__":
uvicorn.run("main:app", host="127.0.0.1", port=8000,
reload=True)
You now have to run the preceding program in the command terminal
as follows:
python main.py
In either case, the log in Listing 2-4 indicates that the application is
running at port 8000 of the localhost.
33
Chapter 2 Getting Started with FastAPI
34
Chapter 2 Getting Started with FastAPI
or set the host parameter to this value in the call to the run() function in
the main.py code (Listing 2-5).
import uvicorn
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def index():
return {"message": "Hello World"}
if __name__ == "__main__":
uvicorn.run("main:app", host="0.0.0.0", port=8000,
reload=True)
35
Chapter 2 Getting Started with FastAPI
Open the browser on any other device on the same network, and enter
the IP address shown in bold letters earlier (keep the port as 8000 as it hasn’t
been changed). The URL to be used is http://192.168.1.211:8000. The
browser shows the same Hello World message as earlier.
36
Chapter 2 Getting Started with FastAPI
Swagger UI
Swagger UI’s web interface is built with the help of HTML, JavaScript, and
CSS assets to autogenerate an interactive documentation based on the
API code. Here, we shall be using it along with the REST API written with
FastAPI.
To understand how Swagger UI documentation works, let us first add
one more path operation in our Hello World example. Update the main.py
to the code shown in Listing 2-6.
@app.get("/")
async def index():
return {"message": "Hello World"}
@app.get("/{name}/{id}")
async def user(name:str, id:int):
return {"name":name, "id":id}
37
Chapter 2 Getting Started with FastAPI
38
Chapter 2 Getting Started with FastAPI
Click the Try it out button. If there are any parameters, you have
to provide their values (in the next section, we shall see how the path
parameters are passed). A button with the caption Execute appears
(Figure 2-5) in the window.
39
Chapter 2 Getting Started with FastAPI
The parameter values are appended to the fixed portion of the path, as
shown in Figure 2-8.
41
Chapter 2 Getting Started with FastAPI
The JSON response of the server also includes the parameter values in
the response body.
Redoc
Redoc is another open source tool from Redocly that generates the API
documentation from the OpenAPI definitions. It has a responsive design,
having a search bar, a navigation menu, the documentation, and examples
of request and response.
42
Chapter 2 Getting Started with FastAPI
43
Chapter 2 Getting Started with FastAPI
JSON Schema
The Swagger and Redoc tools actually translate the JSON representation
of the API code. They use certain JavaScript and CSS code for an elegant
representation of the raw JSON format in which the API schema is present.
This representation is done in the “application/schema+json” media
type. If you want to find out how the raw OpenAPI schema appears, use the
URL http://localhost:8000/openapi.json in your browser. It displays the
JSON data in Listing 2-7.
44
Chapter 2 Getting Started with FastAPI
{
"openapi": "3.0.2",
"info": {
"title": "FastAPI",
"version": "0.1.0"
},
"paths": {
"/": {
"get": {
"summary": "Index",
"operationId": "index__get",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
}
}
}
}
}
}
}
}
There are many API generation tools available, similar to Swagger and
Redoc. You can easily configure FastAPI to use any of these tools. However,
this is a slightly advanced maneuver and is beyond the scope of this book.
45
Chapter 2 Getting Started with FastAPI
Path Parameters
In HTTP terminology, the path refers to the part of the URL that is trailing
after the server’s name or combination of (IP address:port). This path
can be a mixture of fixed part and a variable part. Generally, the fixed part
refers to a collection of resources available on the server, and the variable
part (which may have one or more values) is used to locate or retrieve a
specific resource from the collection.
Consider the request URL http://mysite.com/employee/Rahul/20.
The intention is to retrieve the details of an employee with the name
Rahul and having the age of 20. The /employee here is the fixed part, and
the latter is the variable part consisting of two values. Obviously, they are
likely to change for every request. How does FastAPI identify the variable
parameters in the path and pass them to the operation function?
As already stated, the FastAPI application object acts as a router,
directing the incoming request from the client to the appropriate handler
function. It checks the request URL matches with the pattern declared in
which operation decorator. Once the decorator is identified, its mapped
operation function is executed, and the response is returned to the client.
If the path string of an operation decorator is expected to include one
or more variable values, it has a placeholder for each. The placeholder is a
valid Python identifier put inside curly brackets.
In Figure 2-11, for the request URL under consideration – http://
mysite.com/employee/Rahul/20 – the path string argument of the
decorator has to be /employee/{name}/{age}. Whenever the client
URL matches with this pattern, the data part is parsed into its respective
variables and passed to the operation function below the decorator as path
parameters, as shown in Figure 2-11.
46
Chapter 2 Getting Started with FastAPI
app = FastAPI()
@app.get("/employee/{name}/{age}")
async def user(name:str, age:int):
return {"name":name, "age":age}
47
Chapter 2 Getting Started with FastAPI
Type Parsing
Since the parameters are defined with type hints, the values picked up
from the URL are parsed to the suggested types whenever possible. For
instance, in the preceding case, the trailing 20 in the URL is passed as an
integer parameter to the get_employee() function. However, try entering
the URL http://localhost:8000/employee/Rahul/Kumar in the browser’s
address bar, and you’ll get the response shown in Listing 2-9.
Query Parameters
If a certain operation function (also called a view function) below the path
decorator has some parameters in addition to the placeholders in the URL
pattern, FastAPI deems them to be query parameters.
To understand this, let us rewrite the @app.get() decorator in the
preceding example, as shown in Listing 2-10.
@app.get("/employee/{name}")
async def get_employee(name:str, age:int):
return {"name":name, "age":age}
The decorator parses the value after /employee into a path parameter
called name. However, its mapped function declares one more parameter –
age. How does the router receive a value for this parameter?
Explore the Swagger documentation for the get_employee() function.
Look at Figure 2-13.
49
Chapter 2 Getting Started with FastAPI
50
Chapter 2 Getting Started with FastAPI
Optional Parameters
While path parameters are always required (the request URL must
contain a value for each identifier in the URL pattern), query parameters
may be declared to have a default value or may be set to be optional. In
Listing 2-11, the query parameter age has a default value.
@app.get("/employee/{name}")
async def get_employee(name:str, age:int=20):
return {"name":name, "age":age}
The Swagger docs of this function (Figure 2-15) reflects the fact that
the required mark on top of the age parameter is removed, and its default
value is 20 (you can of course assign any other value if so required).
51
Chapter 2 Getting Started with FastAPI
As a result, even if the query string is not given in the request URL (as
in http://localhost:8000/employee/Rahul), FastAPI internally appends the
query parameter, and the response sends its default value:
{
"name": "Rahul",
"age": 20
}
To let the client choose to give or not to give a value to any query
parameter, use the Optional type from the typing module and set its
default to None. In Listing 2-12, age is an optional parameter.
@app.get("/employee/{name}")
async def get_employee(name:str, age:Optional[int]=None):
return {"name":name, "age":age}
52
Chapter 2 Getting Started with FastAPI
{
"name": "Rahul",
"age": null
}
Order of Parameters
So, we can now say that the path or the endpoint of a URL comprises a
fixed part, one or more path parameters, and then followed by a query
string that may have one or more query parameters. While the query string
appears last in the path, the path parameters can be interspersed between
two fixed parts. Take a look at Figure 2-16, showing URL parts.
53
Chapter 2 Getting Started with FastAPI
@app.get("/employee/{name}/branch/{branch_id}")
async def get_employee(name:str, brname:str, branch_id:int,
age:Optional[int]=None):
employee={'name':name, 'Branch':brname,
'Branch ID':branch_id, 'age':age}
return employee
The API documentation (Figure 2-17) too underlines the fact that the
order of parameter declaration in the operation function need not be the
same as in the URL path.
54
Chapter 2 Getting Started with FastAPI
Validation of Parameters
The path and query parameter components of the URL are user inputs.
Hence, it is important that their values satisfy certain predefined criteria
before they are forwarded to the operation function, so that the server
doesn’t throw unwanted exceptions.
55
Chapter 2 Getting Started with FastAPI
In all the code examples used so far in this chapter, the path and
query parameters have been declared to be of standard Python types –
we have used int and str, but we can use float and bool as well. However,
the FastAPI operation function can have a path or query parameter as
an object of the Path or Query class, respectively. Both these classes are
available in the fastapi module. The advantage of using a Path or Query
instance as a parameter is that one or more validation constraints can
be applied and some additional metadata can be included so that the
documentation becomes more meaningful.
The Path and Query objects can be instantiated by passing certain
parameters to the corresponding constructors. Their first parameter is
the default value – generally set to None. The rest of the parameters are
all optional keyword parameters. They include numeric constraints, the
maximum and minimum length of string parameters, and the metadata of
path parameters to specify the title, description, alias, etc.
@app.get("/employee/{name}/branch/{branch_id}")
56
Chapter 2 Getting Started with FastAPI
The order of the parameters has been changed because the Python
function requires the nondefault arguments to be declared before those
with default value. The validation checks applied mean that the name (path
parameter) should not be smaller than ten characters, and the length of
brname (query parameter) should be between five and ten characters.
Figure 2-18 shows the Swagger documentation of this function. It
highlights these criteria.
57
Chapter 2 Getting Started with FastAPI
{
"detail": [
{
"loc": [
"path",
"name"
],
"msg": "ensure this value has at least 10 characters",
"type": "value_error.any_str.min_length",
"ctx": {
"limit_value": 10
}
},
{
"loc": [
"query",
"brname"
],
"msg": "ensure this value has at most 10 characters",
"type": "value_error.any_str.max_length",
"ctx": {
"limit_value": 10
}
58
Chapter 2 Getting Started with FastAPI
}
]
}
@app.get("/employee/{name}/branch/{branch_id}")
async def get_employee(branch_id:int, brname:str, age:int,
name:str=Path(None, regex="^[J]|[h]$")):
employee={'name':name, 'Branch':brname, 'Branch ID':branch_
id, 'age':age}
return employee
After starting the application, open the browser and enter http://
localhost:8000/employee/Amar/branch/101?brname=London&age=21 as
the URL. Since the name neither starts from J nor does it end with h, then
you get the error response shown in Listing 2-17.
59
Chapter 2 Getting Started with FastAPI
{
"detail": [
{
"loc": [
"path",
"name"
],
"msg": "string does not match regex \"^[J]|[h]$\"",
"type": "value_error.str.regex",
"ctx": {
"pattern": "^[J]|[h]$"
}
}
]
}
60
Chapter 2 Getting Started with FastAPI
61
Chapter 2 Getting Started with FastAPI
"limit_value": 100
}
},
{
"loc": [
"query",
"age"
],
"msg": "ensure this value is greater than or
equal to 20",
"type": "value_error.number.not_ge",
"ctx": {
"limit_value": 20
}
}
]
}
Adding Metadata
The metadata-related properties of Path() and Query() constructors allow
certain descriptive features to the API documentation. These properties
do not have any influence on the validation process. They just add some
additional details about the parameter – such as title, description, etc.
You can add a suitable title and some explanatory text as the
description for the parameter. The alias property can be used if the
placeholder identifier in the endpoint mentioned in the decorator is
different from the formal argument in the function. In the example in
Listing 2-20, the name as a path parameter is customized with these
metadata properties.
62
Chapter 2 Getting Started with FastAPI
63
Chapter 2 Getting Started with FastAPI
Note that we have set the include_in_schema property for age – the query
parameter – to False. As a result, it doesn’t appear in the docs of the function.
S
ummary
This chapter has helped you take the first steps toward learning to build a
web app with FastAPI. We learned what its basic building blocks are. This
chapter has also made you familiar with the API documentation tools –
Swagger and Redoc.
In this chapter, the concept of path and query parameters has been
explained in detail with the help of useful examples and the Swagger tool.
In the end, we explored the provisions to perform parameter validation.
In the next chapter, we shall discuss another concept regarding
inclusion of parameters within the body of the client request with the use
of Pydantic models.
64
CHAPTER 3
Request Body
In the previous chapter, you learned how FastAPI handles the processing
of path and query parameters included in the URL of the client’s GET
request. In this chapter, you will see how you can include required data as
the body part of the client’s HTTP request.
We shall cover the following topics in this chapter:
• POST method
• Body parameters
• Model configuration
• Pydantic fields
• Validation
• Nested models
POST Method
A web browser software can send the request only through the GET
method. We know that a GET request is used to retrieve one or more
resources on the HTTP server. The path and/or query parameters in the
request URL serve as a filter to fetch the data of specific resources.
Using the GET method for client-server interaction over HTTP has
certain drawbacks. First of all, it is not very secure as the URL along with
the parameter data is revealed in the address bar of the browser. Secondly,
there is a limit to how much data can be sent to the server along with the
GET request (and the limit is not very big either – in the range of a few
kilobytes only!). Moreover, the data to be sent must be representable in
ASCII characters only. That means any binary data such as an image can’t
be a part of the GET request.
To send a request for creating a new resource on the server, the HTTP
protocol requires that the POST method should be used. The data that
is required for a new resource is packed in the body of the POST request.
This serves two purposes. The body part is not displayed in the browser’s
address bar; hence, it is a more secure method. Secondly, there is no size
limit, and raw binary data can also be a part of the HTTP request body.
As a web browser cannot be used to raise a POST request directly, we
have to find other means. We can use an HTTP client such as the Curl
command-line tool to send a POST request. A typical example of Curl’s
POST command is shown in Listing 3-1.
Note that we need to set the POST method explicitly with the -X
option (remember that the default HTTP method is GET). The -d option
is followed by the parameters and their values in JSON format. This data
populates the body of the HTTP request.
We can also use certain web-based tools such as the Postman app or
Swagger UI for this purpose or make an HTML form to send the request
submitting the data with the POST method.
66
Chapter 3 Request Body
We have now become fairly conversant with the Swagger UI. We shall
continue to use it in this chapter to understand how the data in the HTTP
body is processed by FastAPI. In a subsequent chapter, we shall deal with
the HTML form data.
Body Parameters
In a FastAPI app, POST requests are handled by the @app.post() decorator. As
explained earlier, the path operation decorator needs a mandatory path string
argument. (If the URL has any path parameters, their placeholder identifiers
may appear in the path string as we learned in the previous chapter.)
The ASGI server passes the request’s context data to the coroutine
function (we call it an operation function) defined just below the
@app.post() decorator. It contains the request object and the body data.
The value of each parameter of the body data is passed to the
corresponding Body parameter declared in the operation function’s
definition. A Body parameter is an object of the Body class in FastAPI
(similar to Path and Query classes).
To process the POST request raised by the Curl command mentioned
earlier, let us define the addnew() operation function under the POST
decorator (Listing 3-2).
@app.post("/product")
async def addnew(request: Request, prodId:int = Body(),
prodName:str = Body(), price:float=Body(), stock:int = Body()):
product={'Product ID':prodId, 'product name':prodName,
'Price':price, 'Stock':stock}
return product
67
Chapter 3 Request Body
HTTP/1.1 200 OK
date: Fri, 26 Aug 2022 17:38:00 GMT
server: uvicorn
content-length: 71
content-type: application/json
The Swagger UI is more convenient to use rather than the Curl tool,
especially while testing the response of the routes of a FastAPI app
(Figure 3-1). So, while the server is running, visit the http://localhost:8000/
docs link with a web browser.
68
Chapter 3 Request Body
69
Chapter 3 Request Body
dataclasses Module
At the center of the power of the Pydantic library is the BaseModel class.
In a way, it is similar to the dataclasses library introduced in Python’s
standard library from version 3.7 onward.
The object of a Python class becomes a data container when decorated
by the @dataclass decorator. It autogenerates the __init__() constructor
for the user’s class and also inserts the __repr__() method for the string
representation of the object.
The Product class decorated by the @dataclass is declared as in Listing 3-4.
Since there’s an autogenerated constructor and string representation method
in place, we can declare the object.
70
Chapter 3 Request Body
stock:int
p1=Product(1, "Ceiling Fan", 2000, 50)
print (p1)
BaseModel
At the center of the Pydantic library’s functionality is the BaseModel class.
A class that uses BaseModel as its parent works as a data container, just
as a dataclass. Additionally, we can apply certain customized validation
criteria on the properties of the class.
Listing 3-5 shows the basic usage of BaseModel. Let's declare the
Product model, based on the BaseModel.
class Product(BaseModel):
prodId:int
prodName:str
price:float
stock:int
71
Chapter 3 Request Body
{
"title": "Product",
"type": "object",
"properties": {
"prodId": {
"title": "Prodid",
"type": "integer"
},
"prodName": {
"title": "Prodname",
"type": "string"
},
"price": {
"title": "Price",
"type": "number"
},
"stock": {
"title": "Stock",
"type": "integer"
}
},
"required": [
"prodId",
"prodName",
"price",
"stock"
]
}
72
Chapter 3 Request Body
class Product(BaseModel):
prodId:int
prodName:str
price:float
stock:int
app = FastAPI()
@app.post("/product/")
async def addnew(product:Product):
return product
The moment FastAPI finds that the operation function has a Pydantic
model parameter, the request body is populated by the properties in the
model class – in our case, the Product class. The class specifications also
help Swagger UI to generate the Product schema. Figure 3-2 shows the
schema part of the documentation.
73
Chapter 3 Request Body
productlist=[]
@app.post("/product/")
async def addnew(product:Product):
productlist.append(product)
return productlist
74
Chapter 3 Request Body
Once the model is passed, its attributes can be accessed and modified
inside the operation function. Here, we would like to apply a tax of 10%
on the price if it is greater than 5000. Listing 3-9 shows how the addnew()
function modifies the price attribute.
@app.post("/product/")
async def addnew(product:Product):
dct=product.dict()
75
Chapter 3 Request Body
price=dct['price']
if price>5000:
dct['price']=price+price*0.1
product.price=dct['price']
productlist.append(product)
return productlist
Model Configuration
The Config attribute of the BaseModel helps in controlling the behavior of
the model. It is in fact an object of the BaseConfig class. This configuration
feature can be used in many ways. For example, the max_anystr_length
decides what should be the maximum length for the model’s string
properties. You can also specify if you want the strings to always appear
in upper- or lowercase. Set anystr_upper and/or anystr_lower to True if
you want.
One of the cool Config settings is to include a schema_extra property
(Listing 3-10). Its value is a dict object giving a valid example of the model
object. This acts as additional information in the documentation of the
JSON Schema of the model.
The Product model with schema_extra defined in its Config is
rewritten in Listing 3-10.
class Product(BaseModel):
prodId:int
prodName:str
price:float
stock:int
76
Chapter 3 Request Body
class Config:
schema_extra = {
"example": {
"prodId": 1,
"prodName": "Ceiling Fan",
"price": 2000,
"stock": 50
}
}
orm_mode
The Config class inside the Pydantic model has an important property
called orm_mode. If it is set to True, the Pydantic model can be created
from any ORM model instance.
The term ORM stands for object-relational mapper. It is used for the
programming technique of mapping a table structure in a SQL database
77
Chapter 3 Request Body
Base = declarative_base()
class ProductORM(Base):
__tablename__ = 'products'
prodId = Column(Integer, primary_key=True, nullable=False)
78
Chapter 3 Request Body
prod_alchemy = ProductORM(
prodId=1,
prodName='Ceiling Fan',
price=2000,
stock=50
)
product = Product.from_orm(prod_alchemy)
Pydantic Fields
A model in FastAPI is simply a Python class inherited from Pydantic’s
BaseModel. Its class attributes become the fields in the model. The Product
model used in the earlier section uses Python’s built-in data types as type
hints while declaring the fields in a model. The data types in the typing
module consisting of type hints for collection data types can also be used.
Hence, we can declare Pydantic fields to be of typing.List, typing.Tuple,
and typing.Dict.
Let us define a Student model. The fields StudentID and name are
of int and str types. In Listing 3-15, we have declared a marks field of
typing.Dict type to store subject-wise marks.
class Student(BaseModel):
StudentID:int
name:str
subjects:Dict[str, int]
class Student(BaseModel):
StudentID:int
name:str
80
Chapter 3 Request Body
subjects:Dict[str, int]
app=FastAPI()
@app.post("/student")
async def addnew(student:Student):
return student
81
Chapter 3 Request Body
the regex property. You can follow the examples in the path parameter
validation section to implement on Fields also.
In addition to Python’s standard data types and the one in the typing
module, the Pydantic library defines its own types.
HttpUrl: This field is essentially a string with built-in validation for
URL schemes applied. The HttpUrl type accepts HTTP as well as HTTPS
schemes. It requires a TLD (top-level domain) and host allowing a
maximum length of 2083 characters.
The AnyUrl type allows any scheme (TLD is not required, but
host required). The FileUrl field is for storing the file URL and doesn’t
require a host.
EmailStr: It is also a string and must be a valid email address. The
validation of email representation of a string requires the email-validator
module to be installed.
SecretStr: This data type is used mostly to store passwords or
any other sensitive information that should not appear in logging or
tracebacks. On conversion to JSON, it will be formatted as '**********'.
Json: Pydantic loads a raw JSON string, not the field of Json type. You
can also use it to parse the loaded object into another type, based on the
type Json is parameterized with.
Validation
The single most important feature of Pydantic is its capability to validate
the input data before passing to the operation function. It has the built-in
validation rules for all data types – standard types, those from the typing
module, as well as Pydantic’s own data types.
In the example (Listing 3-17), the Employee model implements the
Pydantic data types explained earlier.
82
Chapter 3 Request Body
class Employee(BaseModel):
ID: str
pwd: SecretStr
salary: int
details: Json
FBProfile: HttpUrl
app=FastAPI()
@app.post("/employee")
async def addnew(emp:Employee):
return emp
83
Chapter 3 Request Body
{
"ID": "A001",
"pwd": "**********",
"salary": 25000,
"details": {
"Designation": "Manager",
"Branch": "Mumbai"
},
"FBProfile": "https://www.facebook.com/dummy.employee/"
}
If, however, the JSON value of the details field is erroneous, the server
response indicates the reason (Listing 3-20), owing to Pydantic’s built-in
validation.
{
"detail": [
{
"loc": [
"body",
"details"
],
"msg": "JSON object must be str, bytes or bytearray",
"type": "type_error.json"
}
]
}
84
Chapter 3 Request Body
{
"detail": [
{
"loc": [
"body",
"FBProfile"
],
"msg": "invalid or missing URL scheme",
"type": "value_error.url.scheme"
}
]
}
Custom Validation
In addition to the built-in validations, you can provide your own. We need
to use the @validator decorator in Pydantic.
In the Employee model used in the previous example, we have the
ID field of string type. Let us impose a condition on its allowed value. It
is desired that the string should have only the alphanumeric characters.
If any character is non-alphanumeric, the validation should fail, and an
appropriate error message should be generated (Listing 3-22).
Python’s string class has the isalnum() method which returns false if
any character in the string is non-alphanumeric. We use the same in the
static function defined inside the Employee class. The method is decorated
by the @validator decorator. Listing 3-22 shows the new version of the
Employee model.
85
Chapter 3 Request Body
class Employee(BaseModel):
ID: str
pwd: SecretStr
salary: int
details: Json
FBProfile: HttpUrl
@validator('ID')
def alphanum(cls, x):
if x.isalnum()==False:
raise (ValueError('Must be alphanumeric'))
To test the validation, give #001 as the value of the ID field. The
Uvicorn server’s response (Listing 3-23) throws the ValueError with the
error message as mentioned in the preceding code.
{
"detail": [
{
"loc": [
"body",
"ID"
],
"msg": "Must be alphanumeric",
"type": "value_error"
}
]
}
86
Chapter 3 Request Body
Nested Models
As Pydantic models are in fact Python classes, we can always build a
hierarchy of classes by defining a property (field) of a model as the object
of an already defined model class. Listing 3-24 schematically represents
the nesting of models.
ModelA:
..
..
ModelB:
ID:int
a1=ModelA
..
The a1 field of the ModelB class is the object of the ModelA class,
which must be defined earlier.
Let us construct a more realistic nested structure of models. The
Products model, in addition to the ProductID, Name, and price, has a
supplier field. It is a list of objects of the Suppliers model (as more than
one supplier may be dealing in a product).
So, we define the Suppliers model first as in Listing 3-25.
class Suppliers(BaseModel):
supplierID:int
supplierName:str
87
Chapter 3 Request Body
class Products(BaseModel):
productID:int
productName:str
price:int
suppler:List[Suppliers]
class Customers(BaseModel):
custID:int
custName:str
products:List[Products]
app = FastAPI()
@app.post("/customer")
async def getcustomer(c1:Customers):
return c1
88
Chapter 3 Request Body
The request body is filled with customer details. Let us say, two
products are purchased. Details of each product are obtained from the
Products model. Each product has a list of suppliers whose fields are
fetched from the Customers model.
Use the example data in Listing 3-28 to test the /customer route that
invokes the getcustomer() operation function.
{
"custID": 1,
"custName": "C1",
"products": [
{
"productID": 1,
"productName": "P1",
"price": 100,
"suppler": [
{
"supplierID": 1,
"supplierName": "S1"
},
{
"supplierID": 2,
"supplierName": "S2"
}
]
},
{
"productID": 2,
"productName": "P2",
"price": 200,
89
Chapter 3 Request Body
"suppler": [
{
"supplierID": 3,
"supplierName": "S3"
},
{
"supplierID": 4,
"supplierName": "S4"
}
]
}
]
}
90
Chapter 3 Request Body
Summary
This concludes our discussion of Pydantic models. We have learned how
FastAPI uses Pydantic to populate the request body. We also explored the
Pydantic field types and how to perform validations. An important feature
of model configuration to enable its ORM mode has been explained here.
91
Chapter 3 Request Body
It will be very important when we’ll extend our REST API application to
connect with a database.
In the next chapter, we are going to study how certain dynamic data
from the operation function is inserted into web templates, particularly the
Jinja templates.
92
CHAPTER 4
Templates
FastAPI, as its name suggests, is primarily intended to be a library for
API development. However, it can very well be used to build web apps
of traditional type, that is, the ones rendering web pages, images, and
other assets.
In this chapter, we shall learn how FastAPI renders web pages with the
help of templates.
This chapter consists of the following topics:
• HTML response
• Template engine
HTML Response
By default, the response returned by the operation function is of JSON
type. To be precise, an object of JSONType class is returned. However,
app = FastAPI()
@app.get("/")
async def index():
ret='''
<html>
<body>
<h2>Hello World!</h2>
</body>
</html>
'''
return Response(content=ret, media_type="text/html")
94
Chapter 4 Templates
app = FastAPI()
@app.get("/{name}", response_class=HTMLResponse)
async def index(name):
ret='''
<html>
<body>
<h2 style="text-align: center;">Hello {}!</h2>
</body>
</html>
'''.format(name)
return HTMLResponse(content=ret)
The format() function of the str class inserts the value of the path
variable at the placeholder inside the string. The HTML parser engine of
the browser takes care of the tags and their attributes, thereby displaying
the result as in Figure 4-1.
95
Chapter 4 Templates
Template Engine
HTML, the language used to construct web pages, is basically static in nature.
Though we managed to add a certain interactivity in the preceding example
by mixing the string representation of HTML code with a variable part, it is
indeed a very cumbersome approach. Imagine how difficult it will be if you
have to display a tabular data with some columns filled conditionally.
Most modern web application frameworks use a web template system
for such a purpose. It inserts dynamically changing data items at appropriate
places inside the well-designed static web pages. A web template system has
three parts: a template engine, a data source, and a web template.
A web template is essentially a web page with one or more blocks of
a certain template language code with the help of which the web page is
populated with certain dynamic data items. On the other hand, the data
to be interspersed in the web page is available in some data source in the
form of a database table, a memory array, or a CSV file.
The template engine (also called template processor) receives these
two parts. It fetches one set of data items at a time from the data source
(such as one row from a database table) and puts them at their respective
placeholders in the template to dynamically generate multiple web pages,
one for each row in the data source. The schematic diagram in Figure 4-2
explains the functioning of a template engine.
96
Chapter 4 Templates
97
Chapter 4 Templates
<html>
<body>
<h2 style="text-align: center;">Hello World!</h2>
</body>
</html>
98
Chapter 4 Templates
@app.get("/", response_class=HTMLResponse)
async def index(request: Request):
return templates.TemplateResponse("hello.html", {"request":
request})
app/
│ main.py
│
└───templates/
hello.html
app = FastAPI()
@app.get("/", response_class=HTMLResponse)
async def index(request: Request):
return template.TemplateResponse("hello.html", {"request":
request})
99
Chapter 4 Templates
You should get the Hello World text rendered in the <h2> tag on the
browser page as you run this application.
@app.get("/{name}", response_class=HTMLResponse)
async def index(request: Request, name:str):
return template.TemplateResponse("hello.html", {"request":
request, "name":name})
100
Chapter 4 Templates
Use the context variable name and put it inside double curly brackets –
{{ name }} – as the jinja2 placeholder in place of World in hello.html
(Listing 4-9). The template engine substitutes it with the value of the name
path parameter received at runtime.
<html>
<body>
<h2 style="text-align: center;">Hello {{ name }}!</h2>
</body>
</html>
Template Variables
As stated earlier, the view function inserts the request object and, if
required, any other objects in the template context. The context itself is
a dict object. Hence, the data to be passed to the template must have a
string key.
The key in the context is treated as a template variable. In the
preceding example, the “name” key (with its value being the path
parameter name) is put in double curly brackets so that its runtime value is
substituted in the HTML script.
You can add any Python object in the template context. It may be
of standard data type (str, int, list, dict, etc.), an object of any other
Python class, or even a Pydantic model. Let us have a look at some
examples.
101
Chapter 4 Templates
@app.get("/employee/{name}/{salary}",response_
class=HTMLResponse)
async def employee(request:Request, name:str, salary:int):
data= {"name":name, "salary":salary}
return template.TemplateResponse("employee.html",
{"request": request, "data":data})
To display the values in the web page, obtain them with data.get('name')
and data.get('salary') and put them within double curly brackets. Save the
code (Listing 4-11) as employee.html in the templates folder.
<html>
<body>
<h1>Employee Details</h1>
<h2>Name: {{ data.get('name') }} Salary: {{ data.
get('salary') }}</h2>
</body>
</html>
102
Chapter 4 Templates
If the application is running with the debug mode on, change the URL
to http://localhost:8000/employee/Rahul/27. The application’s response
in the browser should be as in Figure 4-3.
103
Chapter 4 Templates
{% if expr1==True %}
HTML block 1
{% elif expr2==True %}
HTML block 2
{% else %}
HTML block 3
{% endif %}
<html>
<body>
<h1>Employee Details</h1>
<h2>Name: {{ data.get('name') }} </h2>
<h2>Salary: {{ data.get('salary') }}</h2>
{% if data.get('salary')>=25000 %}
<h2>Income Tax : {{ data.get('salary')*0.10 }}</h2>
{% else %}
<h2>Income Tax : Not applicable</h2>
{% endif %}
</body>
</html>
104
Chapter 4 Templates
When rendered, the template shows the display (Figure 4-4) on the
browser’s page.
Loop in Template
If any sequence object, that is, a list, tuple, or string, is passed as a context,
it can be traversed inside the template with a looping construct. You have
two keywords for and endfor for this purpose in jinja2. The syntax of the
for statement is similar to the one in Python. Both the keywords, for and
endfor, are put inside {% %} symbols. A typical for loop in jinja2 code
looks like this:
One can of course use a conditional block within the loop as well as
construct nested loops.
In the operation function, a list object langs is passed in the template
context. Save the code in Listing 4-13 as main.py.
105
Chapter 4 Templates
@app.get("/profile/", response_class=HTMLResponse)
async def info(request:Request):
data={"name":"Ronie", "langs":["Python", "Java", "PHP",
"Swift", "Ruby"]}
return template.TemplateResponse("profile.html",
{"request":request,
"data":data})
<html>
<body>
<h2>Name: {{ data.get('name') }} </h2>
<h3>Programming Proficiency</h3>
<ul>
{% for lang in data.get('langs') %}
<b><li> {{ lang }}</li></b>
{% endfor %}
</ul>
</body>
</html>
106
Chapter 4 Templates
app.mount("/static", StaticFiles(directory="static"),
name="static")
107
Chapter 4 Templates
<script src="path/to/myscript.js"></script>
@app.get("/testjs/{name}", response_class=HTMLResponse)
async def jsdemo(request:Request, name:str):
data={"name":name}
108
Chapter 4 Templates
return template.TemplateResponse("static-js.html",
{"request":request,
"data":data})
<!DOCTYPE html>
<html>
<head>
<title>My Website</title>
<script src="{{ url_for('static', path=myscript.js')
}}"></script>
</head>
<body>
<h2>Using JavaScript in Template</h2>
<h3> Welcome {{ data.get('name') }}</h3>
<button onclick="myFunction()">Submit</button>
<p id="response"></p>
</body>
</html>
109
Chapter 4 Templates
function myFunction() {
let text;
if (confirm("Do You Want to Continue\nChoose Ok/Cancel")
== true) {
text = "You pressed OK!";
} else {
text = "You pressed cancel";
}
document.getElementById("response").innerHTML = text;
}
This function pops up the Confirm box. The user’s response (Ok or
Cancel) is placed in the paragraph element with response as its ElementId.
Start the Uvicorn server and enter http://localhost:8000/testjs/Andy in
the browser, as shown in Figure 4-6. It displays the button.
Assuming that the user clicks the OK button, the output is as shown in
Figure 4-7.
110
Chapter 4 Templates
Static Image
To display an image in a web page, we normally use the <img> tag with its
usual syntax as follows:
<img src="path/to/image_file">
The FastAPI logo will be displayed on the web page. FastAPI expects
the static images to be made available in the static folder. We need to
add lines from Listing 4-18 as the operation function in our application
code file.
111
Chapter 4 Templates
@app.get("/img/", response_class=HTMLResponse)
async def showimg(request:Request):
return template.TemplateResponse("static-img.html",
{"request":request})
<!DOCTYPE html>
<html>
<head>
<title>My Website</title>
</head>
<body>
<h2 style="text-align: center;">Static image in
Template</h2>
<img src="{{ url_for('static', path='fa-logo.png') }}" >
</body>
</html>
112
Chapter 4 Templates
113
Chapter 4 Templates
<html>
<head>
<link href="{{ url_for('static', path='mystyle.css') }}"
rel="stylesheet">
</head>
<body>
<h2>Name: {{ data.get('name') }} </h2>
<h3>Programming Proficiency</h3>
<ul class="b">
{% for lang in data.get('langs') %}
<b><li> {{ lang }}</li></b>
{% endfor %}
</ul>
</body>
</html>
h2 {
text-align: center;
114
Chapter 4 Templates
ul.a {
list-style-type: circle;
}
ul.b {
list-style-type: square;
}
115
Chapter 4 Templates
<html>
<head>
<link href="{{ url_for('static', path='formstyle.css') }}"
rel="stylesheet">
</head>
<body>
<h3>Application Form</h3>
<div>
<form action="/form/" method="POST">
<label for="Name">Name of Applicant</label>
<input type="text" id="name" name="name">
<label for="Address">Address</label>
<input type="text" id="add" name="add">
<label for="Post">Post</label>
<select id="Post" name="Post">
<option value="Manager">Manager</option>
<option value="Cashier">Cashier</option>
<option value="Operator">Operator</option>
</select>
</body>
</html>
116
Chapter 4 Templates
This template is served when the browser requests the root URL http://
localhost:8000/ (Figure 4-10).
117
Chapter 4 Templates
define the getform() function) should have these parameters. They must
be objects of the Form class. Moreover, since the form’s method attribute is
POST, the operation decorator should be @app.post().
Before we add the post decorator and its function, we need to install
the python-multipart package. FastAPI needs it to process the forms:
Import this package and add the getform() function in our application
code (Listing 4-23). It parses the user’s data in Form objects. The function
returns a JSON response of the values in the form of a dictionary.
You can test the POST operation with Swagger UI. Enter the test data as
shown in Figure 4-11.
118
Chapter 4 Templates
119
Chapter 4 Templates
Summary
We hereby conclude this chapter on templates in FastAPI. We have learned
how FastAPI renders the jinja2 template. Conditional and loop statements
in the jinja2 template language have also been explained with appropriate
examples. Lastly, we have discussed how to retrieve the data posted by an
HTML form in a view function.
120
CHAPTER 5
Response
Any web application returns an HTTP response to the client. The operation
function in a FastAPI app returns a JSON response by default. In this
chapter, we shall learn how we can manipulate the response to handle
different requirements.
In this chapter, the following points will be discussed:
• Response model
• Cookies
• Headers
• Response types
Response Model
You can declare any operation decorator with an additional response_
model parameter so that the function adopts the response to the specified
Pydantic model.
With the help of response_model, FastAPI converts the output data to
a structure of a model class. It validates the data and adds a JSON Schema
for the response in the OpenAPI path operation.
Consider the case of a Pydantic model called Product (Listing 5-1)
having its field structure.
class Product(BaseModel):
prodId:int
prodName:str
price:float
stock:int
Inventory_val:float
class ProductVal(BaseModel):
prodId:int
prodName:str
Inventory_val:float
122
Chapter 5 Response
We’ll ask FastAPI to use this ProductVal model as its response type.
To do this, use the response_model attribute as a parameter to the @app.
post() decorator, and set it to our required model.
The client request brings the attributes of the Product class as the
body parameters into the operation function. We’ll have to compute the
Inventory_val inside the function (Listing 5-3). As we return the Product
object, the response_model is used to formulate the response; as a result,
the price and stock attributes will not appear in it.
@app.post("/product/", response_model=ProductVal)
async def addnew(product:Product):
product.Inventory_val=product.price*product.stock
return product
Let us check the behavior with the help of Swagger UI. Enter the body
parameters as shown in Figure 5-1.
123
Chapter 5 Response
In Figure 5-2, we see that FastAPI formulates its response to the client
as per the response_model set in the operation decorator.
124
Chapter 5 Response
Cookies
When the server receives a request for the first time from any client
browser, sometimes it may insert some additional data along with the
response. This small piece of data is called a cookie. It is stored as a text file
in the client’s machine.
With the help of cookies, the web application keeps track of the user’s
activities, preferences, etc. On every subsequent visit by the same client,
the cookie data already stored in its machine is also attached to the HTTP
request body and the parameters. The cookie mechanism is a reliable
method by which the web application can store and retrieve a stateful
information regarding the client’s usage, although HTTP is a stateless
protocol.
Let us see how to set and retrieve cookies in a FastAPI application.
set_cookie() Method
The set_cookie() method of the Response object makes setting a cookie very
simple. Let us understand this with an example. Have a look at Listing 5-4.
Here’s a simple login form (form.html) that sends the User ID and password
to the server.
125
Chapter 5 Response
The collection of cookies is a dict object with key-value pairs. The set_
cookie() method can also take max_age and expires as optional parameters.
The setcookie() function in Listing 5-6 sets a “user” cookie with the
Form parameter user as its value.
app = FastAPI()
@app.post("/setcookie/")
async def setcookie(request:Request, response: Response,
user:str=Form(...), pwd:str=Form(...)):
response.set_cookie(key="user", value=user)
return {"message":"Hello World"}
Cookie Parameter
How does the server read the cookies that come along with the client’s
request? The fastapi module defines the Cookie class. When its object is
declared as one of the parameters of an operation function, the retrieved
cookies are stored in it.
126
Chapter 5 Response
The index() function mapped to the “/” URL path has user as the
cookie parameter. (The name of the parameter must be the same as the
cookie previously set. If not, its value will be None.) This parameter is
passed as the context to the HTML template (Listing 5-7).
template = Jinja2Templates(directory="templates")
@app.get("/", response_class=HTMLResponse)
async def index(request: Request, user:str = Cookie(None)):
return template.TemplateResponse("form.html",
{"request":
request,"user":user})
We need to add the template variable in the HTML script of our form.
html so that it reads as in Listing 5-8.
<html>
<body>
{% if user %}
<h3>You are logged in as {{ user }}</h3>
{% endif %}
<hr>
<form action="/setcookie" method="POST">
<h3>User ID</h3>
<p><input type="text" name="user"></p>
<h3>Password</h3>
<p><input type="password" name="pwd"></p>
127
Chapter 5 Response
As a result, when the user visits “/” for the first time, the login form is
displayed as in Figure 5-3.
Click the Submit button to post the form data to the /setcookie path.
The operation function sets the user cookie. When the client revisits the
“/” path, the form is rendered below the message showing the name of the
user who is currently logged in. Figure 5-4 shows how it appears.
128
Chapter 5 Response
Headers
Just as cookies, a web application may push a certain metadata in the form
of HTTP headers into its response. In addition to the predefined HTTP
header types, the response may include custom headers. To set a custom
header, its name should be prefixed with “X”. In the example (Listing 5-9),
the operation function adds a custom header called “X-Web-Framework”
and a predefined header “Content-Language” along with the content to
its response.
@app.get("/header/")
async def set_header():
content = {"message": "Hello World"}
headers = {"X-Web-Framework": "FastAPI", "Content-
Language": "en-US"}
129
Chapter 5 Response
In response to the /header URL, the browser displays only the Hello
World message. To check if the headers are properly set, you need to check
the Swagger documentation of the set_header() function, as shown in
Figure 5-5.
The newly added headers will appear in the response headers section
of the documentation.
Header Parameter
To read the values of an HTTP header from the client request, import the
Header class from the FastAPI library, and use its object as a parameter in
operation function definition. The name of the parameter should match
with the HTTP header converted in camel_case. If you try to retrieve the
“accept-language” header, “-” (dash) in the name of the identifier (since
Python doesn’t allow it) is replaced by “_” (underscore).
130
Chapter 5 Response
As shown in Listing 5-10, add the decorator and its function in the
application code.
131
Chapter 5 Response
As mentioned earlier, the default status code is 200 OK. To include any
other code in the response, use the status_code parameter in FastAPI’s
operation decorator (Listing 5-11).
132
Chapter 5 Response
@app.get("/hello/{name}", status_code=201)
async def sayhello(name: str):
return {"message": "Hello "+name}
133
Chapter 5 Response
Response Types
As mentioned earlier, FastAPI returns its response in JSON form by default,
by automatically converting the return value of the operation function with
the help of json-encoder.
JSONResponse is a subclass of the Response class. You can directly
return the object of the Response class, specifying the media_type such as
media_type="application/xml", "text/html", etc.
For example, in Listing 5-13, the function renders the response with
the media type set as “text/html.”
app = FastAPI()
@app.get("/html/")
def get_html():
data = """
<html>
<body>
<h3>Hello World</h3>
</body>
</html>
"""
return Response(content=data, media_type="text/html")
134
Chapter 5 Response
HTMLResponse
The HTMLResponse class is derived from the Response class, with media_
type set to “text/html.” So, in the preceding example, we can easily replace
the return statement with this:
return HTMLResponse(content=data)
@app.get("/html/", response_class=HTMLResponse)
def get_html():
data = """
<html>
<body>
<h3>Hello World</h3>
</body>
</html>
"""
return Response(content=data)
135
Chapter 5 Response
@app.get("/", response_class=HTMLResponse)
async def index(request: Request):
return templates.TemplateResponse("hello.html", {"request":
request})
JSONResponse
JSON is the default media type of FastAPI’s response. If you are certain
that data returned by the function is serializable with JSON, you can pass it
directly to the response class and avoid the extra overhead (Listing 5-16).
@app.get("/json", response_class=JSONResponse)
def get_html():
data = "Hello World"
return Response(content=data)
StreamingResponse
This is a special type of Response object that takes either an async
generator or a normal generator and streams its yield as the response.
In Python, a generator is a function that produces an iterator, and every
time the yield statement is reached, the next value in the iterator is given out
to the calling environment. Thus, it streams the series of values in the iterator.
In the example (Listing 5-17), the generator() function is a coroutine
that yields a sequence of numbers, which are streamed as the application’s
response by the operation function.
136
Chapter 5 Response
@app.get("/")
async def main():
return StreamingResponse(generator())
On the client browser, you’ll get the streaming line numbers displayed.
A disk file acts as a stream. Python’s file object is an iterator. You can
create a generator function that returns an iterator out of the file object.
Every time the yield statement is executed, it streams the next line in
the file. This is especially useful for bigger files as it is not necessary to
read it all first in memory. Instead, pass the generator function to the
StreamingResponse, and return it.
The code in Listing 5-18 has a readfile() generator that yields lines
from the file. This generator is used by the StreamingResponse.
file="large_file.txt"
@app.get("/")
def index():
def readfile():
with open(file, mode="rb") as f:
yield from f
You can even return the stream of MP4 video bytes as the response.
Just change the media_type to video/mp4.
137
Chapter 5 Response
FileResponse
Note that the open() function that returns the file object doesn’t support
async and await. Hence, the operation function in the preceding example
is not a coroutine but a normal function.
The FileResponse class is more suitable for streaming a file as the
application’s response. A few additional arguments may be given to
instantiate the FileResponse object:
In the example (Listing 5-19), the Uvicorn server streams the video
content on the client browser.
file="wildlife.mp4"
@app.get("/", response_class=FileResponse)
async def index():
return file
Start the server and visit the http://localhost:8000/ URL. The video
starts playing in the browser. A screengrab of the video is shown in
Figure 5-8.
138
Chapter 5 Response
RedirectResponse
The mechanism of redirection in HTTP (called HTTP redirect) is a
special kind of response. It is either used to redirect the user during site
maintenance or downtime (this is a temporary redirect), or for Permanent
redirect in situations like changing the site’s URLs.
Redirect responses have status codes that start with 3. In FastAPI, the
RedirectResponse class implements the HTTP redirect. By default, its
status code is 307 – indicating a temporary redirect.
Let us see the use of RedirectResponse with the example in
Listing 5-20.
Here, we have the index() function that renders a login form web page
template. However, the form script is being modified, and hence the user
needs to be prevented from accessing it and instead should be directed to
another URL indicating that the page is unavailable.
The index() operation function is shown in Listing 5-20.
139
Chapter 5 Response
template = Jinja2Templates(directory="templates")
app = FastAPI()
@app.get("/", response_class=HTMLResponse)
async def index(request: Request):
return RedirectResponse("/redirect")
return template.TemplateResponse("form.html",
{"request": request})
140
Chapter 5 Response
Summary
In this chapter, you learned about the different types of responses that
can be returned by the FastAPI application. The mechanism of including
cookies and headers in the response was also explained in this chapter.
In the next chapter, you’ll learn how to use databases in a FastAPI
application.
141
CHAPTER 6
Using Databases
By now, you must have understood that the FastAPI application consists
of various operation functions. Each operation function is invoked by the
corresponding HTTP method decorator to which it is mapped. The HTTP
methods POST, GET, PUT, and DELETE respectively create a resource,
retrieve one or more resources available with the server, and update or
delete one or more resources.
To perform persistent CRUD operations, the application needs to
interact with a data storage and retrieval system. Applications generally
use relational databases as a back end. Modern web applications,
however, need databases capable of handling huge volume with dynamic
schema, often called NoSQL databases. In this chapter, our objective is to
understand how FastAPI interacts with relational and NoSQL databases.
The following topics are covered in this chapter:
• DB-API
• aiosqlite module
• SQLAlchemy
• async in SQLAlchemy
DB-API
A Python code can communicate with almost any type of relational
database (such as SQLite, MySQL, PostgreSQL, Oracle, and many more).
It needs a database-specific driver interface that is compatible with the
DB-API standards. This ensures that the functionality of performing
database operations is uniform for any type of database. As a result, only
minimal changes will be required if the developer decides to change the
back-end database.
To understand how FastAPI handles the database, we shall use SQLite.
It is a lightweight and serverless database and has a built-in support in
Python’s standard library in the form of a sqlite3 module, the reference
implementation of DB-API. For the other relational databases though, you
need to install the corresponding database driver, such as mysqlclient for
MySQL or Psycopg for PostgreSQL.
In this section, we shall build a FastAPI app to perform CRUD
operations on the Books table in the SQLite database.
import sqlite3
conn=sqlite3.connect("mydata.sqlite3")
144
Chapter 6 Using Databases
cur=conn.cursor()
Call the execute() method on this cursor object. Its string argument
holds the SQL query to be executed by the SQL engine. The Books table
needed for our example is created by the code in Listing 6-3.
def init_db():
conn=sqlite3.connect("mydata.sqlite3")
cur=conn.cursor()
qry='''
SELECT count(name) FROM sqlite_master WHERE type='table'
AND name='Books'
'''
cur.execute(qry)
145
Chapter 6 Using Databases
cur.execute(qry)
conn.close()
init_db()
With the database now created, let us perform further operations on it.
146
Chapter 6 Using Databases
147
Chapter 6 Using Databases
def get_cursor():
conn=sqlite3.connect("mydata.db")
conn.row_factory = sqlite3.Row
cur=conn.cursor()
yield (conn,cur)
app=FastAPI()
@app.post("/books")
def add_book(book: Book, db=Depends(get_cursor)):
id=book.id
title=book.title
author=book.author
price=book.price
publisher=book.publisher
cur=db[1]
conn=db[0]
ins="INSERT INTO books VALUES (?,?,?,?,?)"
cur.execute(ins,(id,title,author,price,publisher))
conn.commit()
return "Record successfully added"
148
Chapter 6 Using Databases
Go ahead and insert a few more books. We can check the records in the
Books table in the SQLite terminal, as shown in Listing 6-8.
149
Chapter 6 Using Databases
150
Chapter 6 Using Databases
151
Chapter 6 Using Databases
@app.get("/books/{id}")
def get_book(id: int, db=Depends(get_cursor)):
cur=db[1]
conn=db[0]
Updating a Book
Listing 6-12 describes the SQL syntax of the UPDATE query.
152
Chapter 6 Using Databases
@app.put("/books/{id}")
def update_book(id:int, price:str=Body(), db=Depends(get_
cursor)):
cur=db[1]
conn=db[0]
qry="UPDATE Books set price=? where id=?"
cur.execute(qry,(price, id) )
conn.commit()
return "Book updated successfully"
Deleting a Book
Conventionally, the HTTP DELETE method is used to remove the
representation of a resource from the server. The URL path pattern for the
path decorator is "/books/{id}", as in Listing 6-14. The ID is passed to the
del_book() function. This function executes the DELETE query.
@app.delete("/books/{id}")
def del_book(id:int, db=Depends(get_cursor)):
cur=db[1]
conn=db[0]
153
Chapter 6 Using Databases
aiosqlite Module
You may have noted that the operation functions in the preceding example
are not coroutines (coroutines are Python functions with the async
keyword in the beginning of the definition). The reason is that the sqlite3
module in Python’s standard library doesn’t support asyncio. Its async-
compatible alternative is the aiosqlite module. As it is not part of the
standard library, we need to install it with the PIP utility:
conn=await aiosqlite.connect("mydata.sqlite3")
cur=await conn.cursor()
The execute() method and the methods to fetch rows from the
queryset are also awaitable (Listing 6-16).
154
Chapter 6 Using Databases
Let us change all the functions in the example in the previous section
so that they can be called asynchronously.
Listing 6-17 gives the coroutine version of the get_cursor() function,
which injects the database context objects into the operation functions.
import aiosqlite
@app.post("/books")
async def add_book(book: Book, db=Depends(get_cursor)):
id=book.id
title=book.title
author=book.author
price=book.price
publisher=book.publisher
cur=db[1]
conn=db[0]
ins="INSERT INTO books VALUES (?,?,?,?,?)"
await cur.execute(ins,(id,title,author,price,publisher))
await conn.commit()
return "Record successfully added"
155
Chapter 6 Using Databases
SQLAlchemy
The CRUD operations in the examples in the previous sections are done
on the SQLite database with sqlite3 and aiosqlite modules. For other
databases, you’ll use respective DB-API-compatible modules (e.g., pymysql
for mysql and aiomysql as its asyncio-compatible version). Inside the path
operation functions, you basically use the request data to construct the
SQL query and then execute it. At times, this can be a tedious task. Various
ORMs (object-relational mappers) make life easy for the developer.
When you are required to work with a relational database from inside
a Python program, you are facing two challenges. Firstly, you must have a
good knowledge of the SQL syntax. Secondly, the conversion of data from a
Python environment to SQL data types is required. The SQL data types are
basically scalar in nature (number, string, etc.), while in Python, you have
objects that may comprise more than one primary data type.
The ORM technique helps in converting data between these
incompatible type systems. The word Object in ORM refers to the
object of a Python class. You may recall that, in the theory of relational
databases, a table is called a relation. A Python class is mapped to a table
of corresponding structure in the database. As a result, each object of the
mapped class reflects as a row in the database table.
The ORM library enables you to manipulate the object data as per the
OO principles. The corresponding SQL statements will be emitted by the
ORM, and the CRUD operations will be done behind the scenes. So, you,
as a Python developer, don’t have to write a single SQL query!
156
Chapter 6 Using Databases
a. Connect to a database
157
Chapter 6 Using Databases
b. ORM model
class Books(Base):
__tablename__ = 'book'
id = Column(Integer, primary_key=True, nullable=False)
title = Column(String(50), unique=True)
author = Column(String(50))
price = Column(Integer)
publisher = Column(String(50))
Base.metadata.create_all(bind=engine)
The create_all() function creates the tables corresponding
to all the classes declared. We have only one, Books class,
mapped to the book table.
c. Session object
158
Chapter 6 Using Databases
You may recall that we need to inject the database context into the path
operation functions of a FastAPI application. We shall use the function in
Listing 6-22 to inject the session object to all the operation functions.
def get_db():
db = session()
try:
yield db
finally:
db.close()
d. Pydantic model
class Book(BaseModel):
id: int
title: str
author:str
159
Chapter 6 Using Databases
price:int
publisher: str
class Config:
orm_mode = True
• POST /books
• GET /books
• GET /books/{id}
• PUT /books/{id}
• DELETE /books/{id}
e. @app.post()
160
Chapter 6 Using Databases
app=FastAPI()
@app.post('/books', response_model=Book)
def add_book(b1: Book, db: Session = Depends(get_db)):
bkORM=Books(**b1.dict())
db.add(bkORM)
db.commit()
db.refresh(bkORM)
return "Book added successfully"
f. @app.get()
Listing 6-25 also has the code for the get_book() function. The
path parameter in the URL parses the ID of the object to be
retrieved. The function fetches the object with the given ID
from the Books model and returns it as the response.
161
Chapter 6 Using Databases
@app.get('/books', response_model=List[Book])
def get_books(db: Session = Depends(get_db)):
recs = db.query(Books).all()
return recs
@app.get('/books/{id}', response_model=Book)
def get_book(id:int, db: Session = Depends(get_db)):
return db.query(Books).filter(Books.id == id).first()
g. @app.put()
@app.put('/books/{id}', response_model=Book)
def update_book(id:int, price:int=Body(), db: Session =
Depends(get_db)):
bkORM = db.query(Books).filter(Books.id == id).first()
bkORM.price=price
db.commit()
return bkORM
162
Chapter 6 Using Databases
h. @app.delete()
@app.delete('/books/{id}')
def del_book(id:int, db: Session = Depends(get_db)):
try:
db.query(Books).filter(Books.id == id).delete()
db.commit()
except Exception as e:
raise Exception(e)
return "book deleted successfully"
You can obtain the complete code for the example explained in this
section from the code repository and try it out.
async in SQLAlchemy
The SQLAlchemy ORM doesn’t yet completely support asynchronous
operations. The latest version of SQLAlchemy (ver. 1.4.40) does have
this feature, albeit on an experimental basis. Furthermore, it hasn’t been
extended to all types of databases.
However, the other branch of SQLAlchemy – called SQLAlchemy
Core – can be used for performing asynchronous database operations with
the help of the databases module.
The SQLAlchemy Core package uses a schema-centric SQL
Expression Language. In this section, we shall briefly explore how the
SQL queries are handled with the Expression Language.
163
Chapter 6 Using Databases
databases Module
The databases module differs a little from the DB-API standard. It
provides functions for connecting to a database, executing queries, and
fetching table data. These functions support async/await. We need to
install the databases module with the PIP installer:
import databases
DATABASE_URL = "sqlite:///./mydata.sqlite3"
db = databases.Database(DATABASE_URL)
async def connection():
await db.connect()
164
Chapter 6 Using Databases
DATABASE_URL = "sqlite:///./mydata.sqlite3"
engine = sqlalchemy.create_engine(DATABASE_URL,
connect_args={"check_same_thread": False})
metadata = sqlalchemy.MetaData()
Add the booklist table in the metadata object with the code in
Listing 6-31.
165
Chapter 6 Using Databases
metadata.create_all(engine)
The select() method of the Table class constructs the SELECT query.
To retrieve one or all the records in the queryset, use the fetch_one() or
fetch_all() function as per the syntax in Listing 6-34.
166
Chapter 6 Using Databases
query=table.select().where(condition)
rows=db.fetch_all(query)
row=db.fetch_one(query)
query=table.update().where(condition).values(field1,value1, ..)
db.execute(query)
query=table.delete().where(condition)
db.execute(query)
This gives a general syntax of how the database table operations are
done. We shall see these functions in action when we develop the path
operations of our FastAPI app.
167
Chapter 6 Using Databases
@app.post("/books", response_model=Book)
async def add_book(b1: Book, db=Depends(get_db)):
query = booklist.insert().values(id=b1.id, title=b1.title,
author=b1.author, price=b1.price, publisher=b1.publisher)
await db.execute(query)
return "Book added successfully"
To retrieve a specific record from the table, the value of its id (it is the
table’s primary key) should be passed as the path parameter. Inside the
get_book() coroutine (in Listing 6-39), the id becomes the condition
for the where clause when the select() method is called. It fetches the
corresponding row, which is returned as the response.
168
Chapter 6 Using Databases
@app.get("/books/{id}")
async def get_book(id: int, db=Depends(get_db)):
query=booklist.select().where(booklist.c.id==id)
return await db.fetch_one(query)
@app.put("/books/{id}")
async def update_book(id:int, new_price:int=Body(),
db=Depends(get_db)):
query=booklist.update().where(booklist.c.id==id).
values(price=new_price)
await db.execute(query)
return "Book updated successfully"
The update() method uses the path parameter – id – to apply the filter
in the where clause.
Finally, the del_book() coroutine is mapped to the DELETE operation
decorator (Listing 6-41). It parses the primary key from the path and
invokes the delete() method of the table class as in Listing 6-41.
@app.delete("/books/{id}")
async def del_book(id:int, db=Depends(get_db)):
query=booklist.delete().where(booklist.c.id==id)
169
Chapter 6 Using Databases
E:\mongodb\bin>mongod
..
waiting for connections on port 27017
170
Chapter 6 Using Databases
You can now interact with the MongoDB server by launching the
MongoDB shell with the Mongo command in another terminal:
E:\mongodb\bin>mongo
MongoDB shell version v4.0.6
connecting to: mongodb://127.0.0.1:27017/?gssapiServiceNa
me=mongodb
Implicit session: session { "id" : UUID("eda5ab88-8ee3-49dd-
8469-1545650f68b1") }
MongoDB server version: 4.0.6
Welcome to the MongoDB shell.
For interactive help, type "help".
..
..
>
171
Chapter 6 Using Databases
You will get to see the list of databases on the server once the
connection is successfully established (Figure 6-3).
172
Chapter 6 Using Databases
173
Chapter 6 Using Databases
def get_collection():
client=MongoClient()
DB = "mydata"
coll = "books"
bc=client[DB][coll]
yield bc
174
Chapter 6 Using Databases
{
"_id": {
"$oid": "63734d09d43843bf5a32f0ba"
},
"bookID": {
"$numberInt": "1"
},
"title": "Programming Basics",
"author": "Robert Ciesla",
"price": {
"$numberInt": "40"
},
"publisher": "Apress"
}
{
"_id": {
"$oid": "6373dd6376dbe609ce2a365c"
},
"bookID": {
"$numberInt": "2"
},
"title": "Decoupled Django",
"author": "Valentino Gag",
"price": {
"$numberInt": "30"
},
"publisher": "Apress"
}
175
Chapter 6 Using Databases
{
"_id": {
"$oid": "6373e3d49f5ae7216c810ca2"
},
"bookID": {"$numberInt": "3"},
"title": "Pro Python",
"author": "Marty Alchin",
"price": {...},
"publisher": "Apress"
}
@app.get("/books", response_model=List[Book])
def get_books(bc=Depends(get_collection)):
books=list(bc.find())
return books
176
Chapter 6 Using Databases
@app.get("/books/{id}", response_model=Book)
def get_book(id: int, bc=Depends(get_collection)):
"""Get all messages for the specified channel."""
b1=bc.find_one({"bookID": id})
return b1
177
Chapter 6 Using Databases
import motor.motor_asyncio
def get_collection():
client=motor.motor_asyncio.AsyncIOMotorClient()
DB = "mydb"
coll = "books"
bc=client[DB][coll]
yield bc
@app.post("/books")
async def add_book(b1: Book, bc=Depends(get_collection)):
result = await bc.insert_one(b1.dict())
return "Book added successfully"
@app.get("/books", response_model=List[Book])
async def get_books(bc=Depends(get_collection)):
books=await bc.find().to_list(1000)
return books
178
Chapter 6 Using Databases
@app.get("/books/{id}", response_model=Book)
async def get_book(id: int, bc=Depends(get_collection)):
b1=await bc.find_one({"bookID": id})
return b1
You can see that using Motor in place of PyMongo doesn’t need many
changes in the code.
Summary
With this, we conclude an important discussion on how to use relational
databases and MongoDB as a back end to a FastAPI app. The classical way
of interacting with the relational databases is through DB-API drivers. For
asynchronous operations on an SQLite database, we learned how to use
the aiosqlite module.
We also learned how FastAPI handles a database with SQLAlchemy
ORM as well as SQLAlchemy Core (with the help of the databases module)
and interacts with Pydantic models.
In the end, we discussed the use of MongoDB in a FastAPI app, both
with PyMongo and Motor drivers.
179
CHAPTER 7
Bigger Applications
So far in this book, we have seen that the FastAPI web app is contained
in a single Python script (conventionally main.py). All the path operation
routes, their respective operation functions, the models, all the required
imports, etc., are put in the same code file.
Things will become messier if the application involves handling of
more than one resources. Take the case of an ecommerce app where CRUD
operations on books as well as music albums are to be performed. Putting
all the HTTP operations for both the products along with their models is
not the ideal way to organize the application code.
In this chapter, we shall learn how to deal with the challenges of
handling bigger applications with more than one API. This chapter
explains the following topics:
• Mounting applications
• Dependencies
• Middleware
• CORS
class book(BaseModel):
id:int
class album(BaseModel):
id:int
app = FastAPI()
@app.get("/")
async def root():
return {"message": "Home page"}
182
Chapter 7 Bigger Applications
You can imagine how lengthy this code would become as you go on
expanding the models with their field structure and functions with their
respective processing logic. Obviously, it will be difficult to debug and
maintain such a lengthy code.
Even the Swagger UI docs page looks very ungainly (Figure 7-1).
183
Chapter 7 Bigger Applications
184
Chapter 7 Bigger Applications
APIRouter
The use of APIRouter allows you to group your routes into different file
structures so that they are easily manageable. All the routes in a bigger
application, such as the preceding example, are clubbed into small units
of APIRouters. The individual APIRouters are then included in the main
application.
Note that these smaller units are not independent applications. They
can be considered to be mini FastAPI apps that are part of the bigger
application. All the routes from all the APIRouters will become a part of the
main application documentation.
Let us implement this concept and rearrange the code in the preceding
application. The routes related to the book resource and the book model
are stored in the books.py file. Similarly, the album routes and album
model go in the albums.py script.
First, declare an object of the APIRouter class (it is in the fastapi
module) instead of the FastAPI class as we normally do (Listing 7-2).
Set the prefix attribute to “/books” and define a tag that appears in the
documentation.
books = APIRouter(prefix="/books",
tags=["books"])
185
Chapter 7 Bigger Applications
books = APIRouter(prefix="/books",
tags=["books"])
class book(BaseModel):
id:int
Note that book – the Pydantic model – is also present in the code.
The albums.py script (Listing 7-4) is along the similar lines.
186
Chapter 7 Bigger Applications
albums = APIRouter(prefix="/albums",
tags=["albums"])
class album(BaseModel):
id:int
187
Chapter 7 Bigger Applications
app = FastAPI()
app.include_router(books.books)
app.include_router(albums.albums)
@app.get("/")
async def root():
return {"message": "Home page"}
Run the server and open the Swagger documentation page. The path
operation functions are nicely grouped together with the prefix tag defined
in each APIRouter declaration.
The books API routes are as in Figure 7-2.
188
Chapter 7 Bigger Applications
Router Package
To further refine the app structure, create two folders inside the
application folder. Name them albums and books. Move the APIRouter
code files albums.py and books.py in the albums and books folders.
Place an empty __init__.py file in both the subfolders so that Python
recognizes them as packages.
Remove the model declarations from the router code and put the same
in models.py in its respective folder. The file structure should be as shown
in Figure 7-4.
189
Chapter 7 Bigger Applications
albums = APIRouter(prefix="/albums",
tags=["albums"])
Note that the album Pydantic model has been moved to the albums/
models.py file, as in Listing 7-7.
class album(BaseModel):
id:int
190
Chapter 7 Bigger Applications
app = FastAPI()
app.include_router(books.books)
app.include_router(albums.albums)
@app.get("/")
async def root():
return {"message": "Home page"}
Mounting Subapplications
The APIRouter class helps you to include a subapplication to the main
FastAPI object. If, on the other hand, you have two (or more) apps that
are independent of each other, having their own API and docs, you can
designate one of them as the main app and mount others.
Earlier in the book (Chapter 4, section “Serving Static Assets”), we
have used the mount() function to make the static assets available at the
“/static” route. The StaticFiles class is really a subapplication. We
mount this subapp on the main application object, so that the static files
are available for use, especially in the templates.
Let us extend this concept to mount the albums and books objects to
the main app. We shall keep the same file and folder structure, where the
albums and books packages are present in the main app folder.
191
Chapter 7 Bigger Applications
albums=FastAPI()
books=FastAPI()
Note that both these apps are stand-alone and can be run
independently.
With the OS command terminal inside the albums folder, run the
following command to start the albums app:
192
Chapter 7 Bigger Applications
app = FastAPI()
@app.get("/stores")
async def root():
return {"message": "Home page"}
app.mount("/albumapi", albums.albums)
app.mount("/bookapi", books.books)
193
Chapter 7 Bigger Applications
Dependencies
A path operation function in a FastAPI app may need parameters in its
context for processing. The user passes path and query parameters in the
request URL itself. On the other hand, the body parameters are read from
the request body, usually POSTed by an HTML form.
194
Chapter 7 Bigger Applications
195
Chapter 7 Bigger Applications
def dow():
from datetime import datetime
dow=datetime.now().weekday()
return dow
Let us now use this function as an argument for Depends in the path
operation coroutine (Listing 7-13).
@app.get("/")
async def root(day=Depends(dow)):
if day==6:
return {"message": "Service not available on Sunday"}
return {"message": "Home page"}
If the user enters the “/” route on Sunday, they get the “Service not
available” message; on other days, the JSON response of the app is a
"Home Page" message.
196
Chapter 7 Bigger Applications
app = FastAPI()
persons=[
{"name": "Tom", "age": 20},
{"name": "Mark", "age": 25},
{"name": "Pam", "age": 27}
]
@app.get("/persons/")
async def get_persons(params: dict = Depends(properties)):
return persons[params['from']:params['to']]
Upon receiving the client request for the “/persons/” URL path,
FastAPI solves the dependency to provide x and y as query parameters.
Check the documentation of the get_persons() function in Figure 7-7.
197
Chapter 7 Bigger Applications
198
Chapter 7 Bigger Applications
The same dependency is also utilized to inject the list indices into the
get_employees() function, as shown in Listing 7-16.
employees=[
{"name": "Tom", "salary": 20000},
{"name": "Mark", "salary": 25000},
{"name": "Pam", "salary": 27000}
]
@app.get("/employees/")
async def get_employees(params: dict = Depends(properties)):
return employees[params['from']:params['to']]
199
Chapter 7 Bigger Applications
app = FastAPI()
@app.get("/")
async def index(request:Request, response: Response):
response.set_cookie(key="user", value="admin")
response.set_cookie(key="api_key", value="abcdef12345")
return {"message":"Home Page"}
When the client requests a GET operation to fetch the list of objects,
the application verifies if the API key has been set or not. We define a
credentials() function in Listing 7-18 for this purpose.
200
Chapter 7 Bigger Applications
def credentials(request):
dct=request.cookies
try:
return dct['api_key']
except:
return None
persons=[
{"name": "Tom", "age": 20},
{"name": "Mark", "age": 25},
{"name": "Pam", "age": 27}
]
@app.get("/persons/")
async def get_persons(key=Depends(credentials(Request))):
if key==None:
return {"message":"API key not validated"}
else:
return persons
201
Chapter 7 Bigger Applications
[
{
"name": "Tom",
"age": 20
},
{
"name": "Mark",
"age": 25
},
{
"name": "Pam",
"age": 27
}
]
class properties:
def __init__(self, x:int, y:int):
self.x=x
self.y=y
202
Chapter 7 Bigger Applications
@app.get("/persons/")
async def get_persons(params: properties =
Depends(properties)):
return persons[params.x:params.y]
class properties:
def __call__(self, x:int, y:int):
self.x=x
self.y=y
return self
@app.get("/persons/")
async def get_persons(params: properties =
Depends(properties())):
return persons[params.x:params.y]
203
Chapter 7 Bigger Applications
@app.get("/employees/")
async def get_employees(params: properties =
Depends(properties())):
return employees[params.x:params.y]
def get_db():
db = session()
try:
yield db
finally:
db.close()
@app.get('/books', response_model=List[Book])
def get_books(db: Session = Depends(get_db)):
recs = db.query(Books).all()
return recs
204
Chapter 7 Bigger Applications
def get_db():
with session() as db:
yield db
Dependency in Decorator
Sometimes, the return value of the dependency function is not required
to be injected into the operation function. But still, you need that
dependency to be solved. In other words, the dependency functions must
be executed before the path operation.
To do so, declare the dependency in the path decorator.
For instance, you want to ensure that the client request contains a
specific custom header. The dependency function (Listing 7-27) raises an
exception if the required header is not detected.
205
Chapter 7 Bigger Applications
Note that this function doesn’t return any value. Hence, it doesn’t
inject any parameter in the operation function. However, we can force
this dependency to be solved by defining the Depends() parameter in the
decorator itself, as in Listing 7-28.
app = FastAPI()
@app.get('/books', dependencies=[Depends(verify_header)])
def get_books(db: Session = Depends(get_db)):
recs = db.query(Books).all()
return recs
If there are more than one dependency, they can be put in a list.
While this applies to a given path operation, you can very well enforce a
global dependency across all the routes in an application. To do so, use
Depends() as an argument in the FastAPI object constructor (Listing 7-29).
app = FastAPI(dependencies=[Depends(verify_header)])
@app.get('/books')
def get_books(db: Session = Depends(get_db)):
recs = db.query(Books).all()
return recs
206
Chapter 7 Bigger Applications
Middleware
A middleware is a function that intercepts every HTTP request before
being processed by the corresponding path operation function. If required,
the function can use the request to perform some process. The request
object is then handed over to the path operation function. The middleware
can also modify the response before it is rendered.
The middleware function is decorated by @app.middleware ("http").
It receives two arguments. The first one is the client request, and the
second is a call_next function.
The call_next function passes the request to the intended path
operation function. Its response may be manipulated by the middleware
before returning to the client.
The simple example (Listing 7-30) shows how the middleware works.
The code has a single route "/" that displays a Hello World message. The
add_header() function intercepts the request, prints a text message on
the console, and passes the request to the path operation function. Before
rendering the response, it adds an X-Framework response header.
@app.middleware("http")
async def add_header(request: Request, call_next):
print ("Message by Middleware before operation function")
response = await call_next(request)
response.headers["X-Framework"] = "FastAPI"
return response
207
Chapter 7 Bigger Applications
@app.get("/")
async def index(X_Framework: Optional[str] = Header(None)):
return {"message":"Hello World"}
208
Chapter 7 Bigger Applications
CORS
The term CORS stands for Cross-Origin Resource Sharing. Imagine a
situation where a certain front-end application running on www.xyz.com
is trying to communicate with a back-end application running on www.
abc.com. Here, the front-end and back-end applications are on different
“origins.” Browsers normally restrict such cross-origin requests.
FastAPI’s CORSMiddleware makes it possible to accept URL requests
from certain domains whitelisted in the application.
To configure the FastAPI app for CORS, import CORSMiddleware and
specify the allowed origins (Listing 7-31).
origins = [
"http://localhost"
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
209
Chapter 7 Bigger Applications
app = FastAPI()
@app.get("/")
async def main():
return {"message": "Hello World"}
While your FastAPI app is running, launch the Apache server and
go to http://localhost:80/hello.html. Click the hyperlink. The browser is
redirected to http://localhost:8000/ which is the URL of your FastAPI app.
You will see the Hello World message displayed.
Summary
Some very important features of FastAPI have been explained in this
chapter. We learned how to build large-scale applications with APIRouter.
We also dived deep into the concept of dependencies and middleware.
In the next chapter, some more advanced features of FastAPI are going
to be discussed. This includes WebSockets, GraphQL, and others.
210
CHAPTER 8
Advanced Features
You now know enough about how to build REST APIs with FastAPI. In this
chapter, you will learn to use modern web applications with WebSocket
and GraphQL technology. We shall also explore the events in FastAPI and
how to include a Flask application.
This chapter is arranged in the following topics:
• WebSockets
• GraphQL
• FastAPI events
• Mounting WSGI application
WebSockets
The HTTP protocol is the backbone of the Internet. We know that HTTP
is a stateless protocol. After sending its response, the server doesn’t hold
back any details about the client. Hence, the client has to reestablish the
connection with the server for each subsequent interaction.
212
Chapter 8 Advanced Features
WebSocket Server
To start up a WebSocket server, call the serve() coroutine defined in the
websockets module and provide ws_handler, host, and port as arguments,
for example:
213
Chapter 8 Advanced Features
ws=asyncio.get_event_loop()
ws.run_until_complete(server)
ws.run_forever()
Now, the handler coroutine is given in Listing 8-2. It basically waits for
the client to request connection, consumes the data it receives, and then
sends back its response.
import asyncio
import websockets
WebSocket Client
On the client side too, you need a coroutine that sends a connection
request to the WebSocket server and asynchronously sends certain
data. As the server responds after acknowledging the request, the client
processes the incoming data. Here’s the client-side coroutine (Listing 8-3).
import asyncio
import websockets
214
Chapter 8 Advanced Features
All you have to do on the client side is to run an asyncio loop till the
coroutine is complete.
Run the server and client code in two separate command terminals.
Input a string in the client window. There should be an immediate
response from the client:
python ws-client.py
215
Chapter 8 Advanced Features
The coroutine defined below (in Listing 8-4) this decorator handles the
WebSocket protocol.
app = FastAPI()
@app.websocket("/test")
216
Chapter 8 Advanced Features
After acknowledging the request, this function runs a loop and sends a
series of random numbers.
What happens on the client side? The client app may be a React or an
Angular application or a mobile app that communicates with the back end
in the native code.
For the sake of simplicity, we’ll use a simple web page as the client.
We also have some JavaScript code in it to handle the WebSocket
communication.
To open a new WebSocket connection within a JavaScript code, use the
special protocol ws in the URL:
Remember, our FastAPI app will run the WebSocket server at the /
test URL.
Once the socket is established, we need to listen to the following
events on it:
There is a button in the HTML page. Its onclick event calls the
handleOnClick() function to send a connection request to the WebSocket
set up by our FastAPI code. With every message sent by the server, the
client receives the random numbers, which are rendered on the page. Save
the HTML script (Listing 8-6) as test.html.
217
Chapter 8 Advanced Features
<script>
var ws = new WebSocket("ws://localhost:8000/test")
ws.onmessage = event => {
var number = document.getElementById("number")
number.innerHTML = event.data
}
handleOnClick = () => {
ws.send("Hi WebSocket Server")
}
</script>
<h3>Streaming numbers appear here</h3>
<div id="number"></div>
<button onclick="handleOnClick()">Click Me</button>
Run the Uvicorn server, and then open the test.html in a browser. As
you click the button in the browser, the test() function in FastAPI code
fires and starts sending random numbers. The numbers start appearing on
the web page until the number generated happens to be 100.
Let us look at a little more elaborate example in Listing 8-7.
The “/” route on the FastAPI server-side code reads a socket.html file
and renders a form.
On the HTML form (Listing 8-8), we have a textbox for sending a text
input to the WebSocket client.
218
Chapter 8 Advanced Features
<h1>WebSocket Client</h1>
<form action="" onsubmit="sendMessage(event)">
<input type="text" id="sendText"/>
<input type="submit" name="send">
<button onclick="handleOnClick()">Close</button>
</form>
<ul id='messages'>
</ul>
Note that <ul> element with messages as its Element Id is used to echo
the sent messages.
The event handler the JavaScript function attached to the Send button
is given in Listing 8-9.
function sendMessage(event) {
var input = document.getElementById("sendText")
ws.send(input.value)
input.value = ''
event.preventDefault()
}
The server just echoes back the received text. Listing 8-10 has the code
for the WebSocket handler coroutine.
@app.websocket("/ws")
async def ws_handler(websocket: WebSocket):
await websocket.accept()
219
Chapter 8 Advanced Features
while True:
data = await websocket.receive_text()
await websocket.send_text(f"Message text was: {data}")
ws.onmessage = function(event) {
var messages = document.getElementById('messages')
var message = document.createElement('li')
var content = document.createTextNode(event.data)
message.appendChild(content)
messages.appendChild(message)
};
handleOnClick = () => {
ws.close();
alert("Connection Closed");
}
220
Chapter 8 Advanced Features
221
Chapter 8 Advanced Features
222
Chapter 8 Advanced Features
class Connection_handler:
def __init__(self):
self.connection_list: List[WebSocket] = []
223
Chapter 8 Advanced Features
manager = Connection_handler()
@app.websocket("/ws/{client_id}")
async def websocket_endpoint(websocket: WebSocket, client_
id: int):
await manager.connect(websocket)
try:
while True:
data = await websocket.receive_text()
await manager.send_message(f"You wrote: {data}",
websocket)
await manager.broadcast(f"Client #{client_id} says:
{data}")
except WebSocketDisconnect:
manager.disconnect(websocket)
await manager.broadcast(f"Client #{client_id} left
the chat")
The hello() function in the FastAPI code for the "/" URL route is kept
as it was in the earlier example.
224
Chapter 8 Advanced Features
The rest of the JavaScript functionality remains the same. In the HTML
body section, provide a span element with wsID as its ID, as in Listing 8-16.
<h1>WebSocket Client</h1>
<h2>Your ID: <span id="wsID"></span></h2>
<form action="" onsubmit="sendMessage(event)">
<input type="text" id="sendText"/>
<input type="submit" name="send">
<button onclick="handleOnClick()">Close</button>
</form>
<ul id='messages'>
</ul>
225
Chapter 8 Advanced Features
GraphQL
Even though REST is widely regarded as the standard for designing web
APIs, the REST APIs tend to be too rigid and hence can’t handle the clients’
requirements that keep changing.
GraphQL was developed to address the shortcomings and
inefficiencies of the REST APIs and provide more flexible and efficient
alternative.
A major drawback of REST is that a client downloads more information
than is actually required for its consumption. The response from an API
endpoint might contain much more info that is superfluous for the client,
as it might need only some details of the resource.
Another issue with REST is that a specific endpoint doesn’t provide
enough information required by the client. Hence, the client might need to
make additional requests to fulfill the client’s requirements.
GraphQL has been developed and open-sourced by Facebook.
Because of its efficiency and flexibility, GraphQL is now recognized as a
new API alternative to REST architecture.
226
Chapter 8 Advanced Features
With its declarative data fetching, the GraphQL client is able to ask for
exactly what data it needs from an API. Another feature of GraphQL is that
a GraphQL server only exposes a single endpoint as against REST where
multiple endpoints are defined. Hence, instead of returning fixed data
structures, the GraphQL server responds with the data precisely as per
client request.
Contrary to a common misconception, GraphQL is not a database
technology. GraphQL is a query language for APIs and has nothing to do
with SQL which is used by databases. In that sense, it is independent of any
type of database. You can use it in any context where you need an API.
Figure 8-5 illustrates the GraphQL architecture.
227
Chapter 8 Advanced Features
type Book {
title: String!
price: Int!
}
We have defined two fields in the Book type here; they’re called
title (which is a string field) and price (which is an int field). In SDL,
the ! symbol after the type indicates that this field is required and cannot
be null.
As mentioned earlier, a GraphQL API exposes only a single endpoint.
On the contrary, a REST API has multiple endpoints that return fixed data
structures. This works because the data structure returned is not fixed.
In fact, it is totally flexible in nature. Therefore, the client can determine
exactly what its requirements are.
However, to define its requirements precisely, the client has to provide
more information to the server in the form of a query.
Queries
Here is an example of the query that can be sent by a client to a server
(Listing 8-18).
{
books {
title
}
}
228
Chapter 8 Advanced Features
The books field in this query is called the root field of the query. The
title field in the root field is called the payload of the query. This query
would return a list of all books currently available.
Here, the response contains only the title of each book, but the price
is not included in the response returned by the server. That’s because
GraphQL allows you to specify exactly what is required. Here, title was
the only field that was included in the query.
You only have to include another field in the query’s payload whenever
needed (Listing 8-19).
{
books {
title
price
}
}
Mutations
Most applications invariably need to modify the data that’s currently
available with the back end. GraphQL uses mutations to make such
changes. There are three types of mutations:
• To delete data
Mutations appear similar in structure as queries, but you must use the
mutation keyword in the definition. To create a new Book, the mutation
can be as shown in Listing 8-20.
229
Chapter 8 Advanced Features
mutation {
createBook(title: "Python Programming", price: 750) {
title
price
}
}
Notice that the mutation type also has a root field – named
createBook. It takes two arguments that specify the title and price of the
new book.
Here is the server response for the preceding mutation:
"createBook": {
"title": "Programming in Python",
"price": 750,
}
Subscriptions
In GraphQL terminology, there is the concept of subscriptions, so that a
real-time connection to the server is available, and the client immediately
gets information about important events.
If a client subscribes to an event, the server pushes the corresponding
data to it, whenever that particular event then actually happens.
Subscriptions don’t follow a typical “request-response-cycle” (as is done
by queries and mutations). They are a stream of data sent over to the client.
The subscription on events happening on the Book type is shown in
Listing 8-21.
230
Chapter 8 Advanced Features
subscription {
newBook {
title
price
}
}
Schema
In GraphQL, the concept of schema is of much importance. How the
client requests the data depends on the definition of schema. It is sort of a
contract that both the server and the client abide by.
A GraphQL schema is a collection of GraphQL root types. The special
root types (those described earlier) are included in the definition of the
schema of an API:
The client requests use Query, Mutation, and Subscription types as the
entry points for communication with the server.
Strawberry GraphQL
Many programming languages, including Python, support the
implementation of the GraphQL communication pattern. The declarative
nature of GraphQL’s Schema Definition Language suits Python nicely.
With newer Python versions providing the capability of asynchronous
processing in the form of asyncio, Python is now a natural choice for many
GraphQL developers.
231
Chapter 8 Advanced Features
There are quite a few Python libraries available for implementing the
GraphQL protocol. Here are some of them:
• Ariadne
• Strawberry
• Tartiflette
• Graphene
We now declare a Book class (Listing 8-22) with title, author, and price
fields and use the @strawberry.type decorator on top of it.
import strawberry
@strawberry.type
class Book:
title: str
author: str
price: int
232
Chapter 8 Advanced Features
@strawberry.type
class Query:
@strawberry.field
def book(self) -> Book:
return Book(title="The Godfather", author="Mario Puzo",
price=750)
schema = strawberry.Schema(query=Query)
graphql_app = GraphQL(schema)
Finally, declare the FastAPI object, and add the URL route "/book",
(as in Listing 8-25) that invokes the callable GraphQL object.
app = FastAPI()
app.add_route("/book", graphql_app)
233
Chapter 8 Advanced Features
Launch the FastAPI application on the Uvicorn server and visit the
http://localhost:8000/book URL in your favorite browser. An in-browser
tool called GraphiQL starts up (Figure 8-6). GraphiQL is a browser-based
user interface with which you can interactively execute queries against a
GraphQL API.
Scroll down below the commented section and enter the query in
Listing 8-26 using the Explorer bar of the GraphiQL IDE.
234
Chapter 8 Advanced Features
author
price
}
}
Run the query to display the result in the output pane of the IDE as
shown in Figure 8-7.
235
Chapter 8 Advanced Features
@strawberry.type
class Mutation:
@strawberry.mutation
def add_book(self, title: str, author: str, price:int)
-> Book:
return Book(title=title, author=author, price=price)
When the browser is directed to the GraphQL endpoint /book, use the
snippet in Listing 8-28 to run the mutation inside the GraphiQL IDE.
mutation {
addBook(
title: "Harry Potter Collection",
236
Chapter 8 Advanced Features
author: "J.K.Rowling",
price:2500) {
title
author
price
}
}
You can always provide mutations for update and delete functionality.
In typical use cases of GraphQL APIs, of course there will be a back-end
database to perform these operations persistently. You can implement the
database operations discussed in the earlier chapter of this book.
237
Chapter 8 Advanced Features
FastAPI Events
If you look carefully at the activities in the command terminal (in which
you start the Uvicorn server), you will find that the event of starting the
application as well as the shutting down is echoed there. You can attach a
handler function to each of these two events. FastAPI has two decorators –
@app.on_event("startup") and @app.on_event("shutdown") – for the
purpose.
The decorated functions will fire as and when these events occur.
Obviously, both events occur once during each run. This feature can be
effectively utilized to perform a mandatory activity as the application starts
and just before the server stops. A typical use case can be starting and
closing the database connection.
In the example in Listing 8-29, a text file is being used by the FastAPI
application to keep a log of the startup and shutdown time of the server.
As the application starts, the startup time is recorded in the file. Similarly,
when the user stops the server by pressing Ctrl+C, the shutdown time is
appended to the file.
Listing 8-29 shows the application code.
app = FastAPI()
@app.on_event("startup")
def startup_event():
print('Server started\n')
log= open("log.txt", mode="a")
log.write("Application startup at {}\n".format(datetime.
datetime.now()))
238
Chapter 8 Advanced Features
log.close()
@app.on_event("shutdown")
async def shutdown_event():
print('server Shutdown :', datetime.datetime.now())
log= open("log.txt", mode="a")
log.write("Application shutdown at {}\n".format(datetime.
datetime.now()))
log.close()
Note that you should start the Uvicorn server in the production
environment and not in dubbed mode (without the --reload flag);
otherwise, the shutdown event will not be raised.
239
Chapter 8 Advanced Features
@flask_app.route("/")
def index_flask():
return "Hello World from Flask!"
@app.get("/")
def index():
return {"message": "Hello World from FastAPI!"}
app.mount("/flask", WSGIMiddleware(flask_app))
240
Chapter 8 Advanced Features
Summary
With this, we are concluding the discussion of some of the advanced
features of the FastAPI framework. We learned that FastAPI is not just
about REST, and it supports the latest API technologies – namely,
WebSockets and GraphQL. We now also know about the FastAPI events
and how to use the Flask app with FastAPI.
In the next chapter, we shall explore the techniques to make our
FastAPI code more robust and failproof, by learning about the security
measures and how to run tests.
241
CHAPTER 9
• Exception handling
• Security
• Testing
• AsyncClient
Exception Handling
The fact that, in any software, a proper exception handling mechanism
is of utmost importance cannot be overemphasized. An exception is a
runtime error situation often leading to an abnormal termination of the
process. Rather than leaving the user clueless about the reason of the
termination, a proper feedback to inform the user about the reason of the
exception is necessary.
In the case of an API, the exception may be caused by various reasons.
For instance, the resource that the client (for an API, the client could be
app = FastAPI()
@app.get("/names/{id}")
async def get_name(id: str):
for name in names:
if id in name.keys():
return {"name": name[id]}
else:
raise HTTPException(status_code=404, detail="Name
not found")
244
Chapter 9 Security and Testing
User-Defined Exception
The Exception classes defined in FastAPI (in addition to the
HTTPException discussed earlier, the WebSocketException class is also
available) inherit Python’s Exception class. Hence, it is entirely possible to
define a custom exception class, subclassing the Exception.
First, define a simple MyException class (Listing 9-2).
class MyException(Exception):
def __init__(self, msg:str):
self.msg=msg
245
Chapter 9 Security and Testing
@app.exception_handler(MyException)
async def myexceptionhanlder(request:Request, e:MyException):
return JSONResponse(status_code=406, content={"message":
"{} was encountered".format(e)})
Now we shall put this custom exception to use in our path operation
function get_name() from the previous example. If the function doesn’t
find the name with the given id path parameter, we first check if it equals
'end' and, if so, raise the newly defined MyException. If not, raise the
HTTPException as done previously. Listing 9-4 provides the modified
definition of get_name() function.
@app.get("/names/{id}")
async def get_name(id: str):
for name in names:
if id in name.keys():
return {"name": name[id]}
else:
if id=='end':
raise MyException(id)
else:
raise HTTPException(status_code=404, detail="Name
not found")
There are three possible scenarios here. First, the path operation is
completed normally in response to the id parameter is present (Figure 9-2).
246
Chapter 9 Security and Testing
And third, the id is not found, but it’s not end either, as in Figure 9-4.
247
Chapter 9 Security and Testing
Security
We know that an API acts as the interface (API indeed is an acronym for
Application Programming Interface) used by two different applications
to transfer data. When an application grants access to the resources of its
server to the outside world, the end users are authenticated and have the
right kind of authorization. Securing an API is an important part of the
development process, equally important if not more than the development
of the application logic itself.
In this context, one often comes across two terms – authentication
and authorization. Seemingly similar, they perform different functions
and together provide a complete security system for an API. While
authentication refers to verification of the identity of the user,
authorization is the process that ascertains the permissions to the user.
248
Chapter 9 Security and Testing
app = FastAPI()
scheme = HTTPBasic()
@app.get("/")
def index(logininfo: HTTPBasicCredentials = Depends(scheme)):
return {"message": "Hello {}".logininfo.username}
When a client browser goes to the “/” URL endpoint for the first time,
a dialog box pops up as in Figure 9-5, prompting the user to provide the
username and the password.
249
Chapter 9 Security and Testing
OAuth
FastAPI has an out-of-the-box support for OAuth2 security standard
specification. OAuth stands for Open Authorization. OAuth version 2.0
provides simple authorization flows for web applications, desktop and
mobile applications, etc.
One of the important features of OAuth is that it enables sharing
information with another service without exposing your password. OAuth
uses “access tokens.” An access token is a random string of alphanumeric
characters. A bearer token (Figure 9-6) is the most commonly used. Once
the OAuth client has the possession of the bearer token, it is able to make
request for the associated resources with the server.
250
Chapter 9 Security and Testing
class User(BaseModel):
id: int
username: str
password: SecretStr
token: str
class Config:
orm_mode = True
class Users(Base):
__tablename__ = 'user'
id = Column(Integer, primary_key=True, nullable=False)
username = Column(String(50), unique=True)
password = Column(String(200))
token = Column(String(200))
251
Chapter 9 Security and Testing
It may be noted here that when using the password flow, the OAuth
standard requires that the client must send the user data for validation
as the values of username and password fields, without any other
variation (which means you cannot have the model attributes as anything
like userId or user-name, or the use of pwd instead of password is not
accepted).
OAuth2PasswordBearer
Let us have a simple GET path operation that just renders a Hello
World message (Listing 9-7). To enforce authentication, apply the
OAuth2PasswordBearer() function as the dependency to the path
operation function.
The OAuth2PasswordBearer class is defined in the fastapi.security
module. Its constructor has a required argument in the form of tokenUrl,
set to a URL route that returns the bearer token.
scheme = OAuth2PasswordBearer(tokenUrl='token')
@app.get('/hello')
async def index(token: str = Depends(scheme)):
return {'message' : 'Hello World'}
With only this much of code in place, try to run the application. The
Swagger UI page already shows its effect, with the Authorize button
appearing prominently, as in Figure 9-7.
252
Chapter 9 Security and Testing
You also get to see the lock icon in front of the index() function. If you
try to execute it, the server responds with a 401 status code, with a Not
Authenticated message, as in Figure 9-8.
253
Chapter 9 Security and Testing
@app.post('/token')
async def token(form_data: OAuth2PasswordRequestForm =
Depends()):
token=hash(form_data.password)
return {'Access Token' : token }
254
Chapter 9 Security and Testing
255
Chapter 9 Security and Testing
Go ahead and test the /hello route. Since the client now has the
authorization token, you’ll now see the Hello World message as its
response.
Let us extend the example to provide the authentication of the
username and password against the user data in a database (Listing 9-9).
We have already defined the models. Using the declarative_base class
from SQLAlchemy, the User table is created in a SQLite database.
256
Chapter 9 Security and Testing
##SQLAlchemy Engine
from sqlalchemy import create_engine
from sqlalchemy.dialects.sqlite import *
SQLALCHEMY_DATABASE_URL = "sqlite:///./mydata.sqlite3"
engine = create_engine(
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_
thread": False})
#Session object
from sqlalchemy.orm import sessionmaker, Session
##Users model
from sqlalchemy import Column, Integer, String
class Users(Base):
__tablename__ = 'user'
id = Column(Integer, primary_key=True, nullable=False)
username = Column(String(50), unique=True)
password = Column(String(200))
token = Column(String(200))
Base.metadata.create_all(bind=engine)
Note that the Users model has a token attribute that stores the
hashed value of password. Let us provide a POST operation function
add_user() to compute the token field and add a new user in the database
(Listing 9-10).
257
Chapter 9 Security and Testing
@app.post('/users', response_model=User)
def add_user(u1: User, db: Session = Depends(get_db)):
u1.token = hash(u1.password)
u1.password=u1.password.get_secret_value()
usrORM=Users(**u1.dict())
db.add(usrORM)
db.commit()
db.refresh(usrORM)
return u1
@app.get('/users', response_model=List[User])
def get_users(db: Session = Depends(get_db), token: str =
Depends(scheme)):
recs = db.query(Users).all()
return recs
If the dependencies are solved, the list of users in the database will be
displayed. However, we need to modify the token() function (in Listing 9-12)
to check the username and password entered by the user in the Password
request form.
258
Chapter 9 Security and Testing
@app.post('/token')
async def token(form_data: OAuth2PasswordRequestForm =
Depends(), db: Session = Depends(get_db)):
u1= db.query(Users).filter(Users.username == form_data.
username).first()
if u1.password == form_data.password:
return {'access_token' : u1.token }
Make sure that all the preceding changes are made, and the server is
running. From the Swagger UI page, authorize the get_users() function.
The server’s response will appear as in Figure 9-11.
259
Chapter 9 Security and Testing
Testing
FastAPI’s testing functionality is based on the HTTPX client library. The
TestClient object can issue requests to the ASGI application. You can
then write useful unit tests and verify their results with PyTest.
As a prerequisite to writing and running unit tests, you need to install
two libraries – HTTPX and PyTest – with the following commands:
Let us first have two path operations – a GET operation and a POST
operation in the main.py script (Listing 9-13). The list() function
retrieves an item from the Books list. The addnew() function is decorated
with @app.post() and adds a book in the list. The main.py script is fairly
straightforward.
class Book(BaseModel):
title: str
price: int
app=FastAPI()
@app.get("/list/{id}")
async def list(id:int):
return books[id-1]
260
Chapter 9 Security and Testing
@app.post("/list", status_code=201)
async def add_new(b1:Book):
books.append(b1.dict())
return b1
One notable thing about the preceding code, especially the POST
decorator, is that a 201 status code is passed to it to imply that a successful
POST operation creates a new resource.
It may be remembered that tests are saved in Python scripts whose
name starts with test_. The test function should also be named as test_*.
We shall write the tests in a test_main.py file. It is placed in the same
folder in which the Python script with the FastAPI app object is declared.
The folder must have an empty __init__.py file for the folder to be
recognized as a package:
C:\fastapi\testing
│ main.py
│ test_main.py
│ __init__.py
client = TestClient(app)
261
Chapter 9 Security and Testing
JSONized response equals the first item in the list (first because we are
passing 1 as the path parameter). Add the test_list() function as in
Listing 9-15.
def test_list():
response = client.get("/list/1")
assert response.status_code == status.HTTP_200_OK
assert response.json() == {"title":"Python", "price":500}
def test_add_new():
response = client.post("/list", json={"title":"Learn
FastAPI", "price":1000})
assert response.status_code == status.HTTP_201_CREATED
Run the tests from the command line. PyTest automatically discovers
the tests and tells if they pass or fail:
(fastenv) C:\fastenv\testing>pytest
================== test session starts ========================
platform win32 -- Python 3.10.7, pytest-7.2.0, pluggy-1.0.0
rootdir: C:\fastenv\testing
plugins: anyio-3.6.2
262
Chapter 9 Security and Testing
collected 2 items
test_main.
py.. [100%]
Testing WebSocket
The TestClient object is also capable of sending a connection request to
a WebSocket set up by a FastAPI endpoint. On the server side, the request
is accepted, and a JSON message is sent to the client – in this case, the test
function. The FastAPI code is very simple (Listing 9-17) – just refer to our
earlier discussion about the WebSockets module in Chapter 8.
app = FastAPI()
@app.websocket("/wstest")
async def wstest(websocket: WebSocket):
await websocket.accept()
await websocket.send_json({"msg": "From WebSocket Server"})
await websocket.close()
As in the previous topic, create an empty __init__.py file, and save the
code shown in Listing 9-18 in test_main.py which should be alongside the
main.py script.
263
Chapter 9 Security and Testing
def test_wstest():
client = TestClient(app)
with client.websocket_connect("/wstest") as websocket:
data = websocket.receive_json()
assert data == {"msg": "WebSocket Server"}
def test_wstest():
client = TestClient(app)
with client.websocket_connect("/wstest") as websocket:
data = websocket.receive_json()
> assert data == {"msg": "WebSocket Server"}
E AssertionError: assert {'msg': 'From...ocket
Server'} == {'msg': 'WebSocket Server'}
E Differing items:
E {'msg': 'From WebSocket Server'} != {'msg':
'WebSocket Server'}
E Use -v to get more diff
test_main.py:7: AssertionError
==================== short test summary info ==================
264
Chapter 9 Security and Testing
Testing Databases
The operation functions underneath the API endpoints primarily perform
CRUD operations on the back-end database. Hence, your tests should
assert their satisfactory execution.
Often, you would like to set up a different database for testing rather
than using the live database. For this purpose, you need to override the
dependency that injects the session object with which you carry out the
CRUD operations.
Override Dependency
To understand what is overriding of dependency and how it works, let us
revisit the example in the section “Query Parameters As Dependencies”
in Chapter 7. Here, the query parameters are injected by the properties()
function. The relevant part of the code is reproduced here:
@app.get("/persons/")
async def get_persons(params: dict = Depends(properties)):
return persons[params['from']:params['to']]
To write and run a unit test, we would rather like to define a new
dependency and override the application’s dependency. The dependency_
overrides property of the FastAPI application object allows you to do this.
Put the function in the test_main.py file (Listing 9-19).
265
Chapter 9 Security and Testing
app.dependency_overrides[properties] = new_properties
With this new dependency, you expect the persons list to contain
only the first entry. We can now assert if it really is the case, with the test
function in Listing 9-20.
client = TestClient(app)
def test_overridden_depends():
response = client.get("/persons/")
assert response.status_code == 200
assert response.json() == [
{
"name": "Tom",
"age": 20
}
]
Override get_db()
While discussing how FastAPI interacts with different databases (Chapter 6),
we have frequently used the get_db() function as dependency and inject
database session reference in the operation functions that perform CRUD
operations.
266
Chapter 9 Security and Testing
class Books(Base):
__tablename__ = 'book'
id = Column(Integer, primary_key=True, nullable=False)
title = Column(String(50), unique=True)
price = Column(Integer)
Base.metadata.create_all(bind=engine)
class Book(BaseModel):
id: int
title: str
price:int
class Config:
orm_mode = True
app=FastAPI()
def get_db():
db = session()
try:
yield db
finally:
db.close()
@app.post('/books', response_model=Book)
def add_book(b1: Book, db: Session = Depends(get_db)):
267
Chapter 9 Security and Testing
bkORM=Books(**b1.dict())
db.add(bkORM)
db.commit()
db.refresh(bkORM)
return b1
SQLALCHEMY_DATABASE_URL = "sqlite:///./test.sqlite3"
engine = create_engine(
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_
thread": False}
)
TestingSession = sessionmaker(autocommit=False,
autoflush=False, bind=engine)
Base.metadata.create_all(bind=engine)
def test_get_db():
try:
db = TestingSession()
268
Chapter 9 Security and Testing
yield db
finally:
db.close()
app.dependency_overrides[get_db] = test_get_db
client = TestClient(app)
def test_add_book():
response = client.post(
"/books/",
json={"id":1,"title": "Jungle Book", "price": 500},
)
assert response.status_code == 200
data = response.json()
assert data["title"] == "Jungle Book"
assert "id" in data
book_id = data["id"]
response = client.get(f"/books/{book_id}")
assert response.status_code == 200
data = response.json()
assert data["title"] == "Jungle Book"
assert data["id"] == book_id
When you run this test, a POST request to the /books endpoint is
initiated with the given data and returns the result of assertion if the
retrieved instance equals the test data.
269
Chapter 9 Security and Testing
AsyncClient
All the tests in this topic so far have been synchronous in nature as the
test functions are defined as normal Python functions and not coroutines
(having an async prefix). That’s because the fastapi.TestClient class
doesn’t support asynchronous calls. Thankfully, we have the HTTPX
library whose AsyncClient class allows us to write async tests.
Asynchronous tests become especially important in an application that
processes the back-end database asynchronously. Earlier in this book, we
have learned how FastAPI handles asynchronous database operations.
For this section, we shall be using the FastAPI code in Listing 9-24.
There is an in-memory database in the form of a list of books and
asynchronous POST and GET operation functions.
Listing 9-24. FastAPI code with GET and POST operation functions
class Book(BaseModel):
title: str
price: int
app=FastAPI()
@app.get("/list/{id}")
async def list(id:int):
return books[id-1]
270
Chapter 9 Security and Testing
@app.post("/list", status_code=201)
async def add_new(b1:Book):
books.append(b1.dict())
return b1
import pytest
from httpx import AsyncClient
from .main import app
@pytest.mark.anyio
async def test_list():
async with AsyncClient(app=app, base_url="http://
localhost") as ac:
response = await ac.get("/list/1")
assert response.status_code == 200
assert response.json() == {"title":"Python",
"price":500}
271
Chapter 9 Security and Testing
Summary
As mentioned in the beginning of this chapter, security and testing are
important steps in the API development process. In this chapter, we
learned a few techniques to provide a secure access to our API. We also
learned how to write and run unit tests with the help of PyTest and HTTPX
libraries.
In the next chapter, we’ll get to know about different ways to deploy a
FastAPI application.
272
CHAPTER 10
Deployment
The journey of web application development culminates when you
make it publicly available for users. Once you are confident that your
app is production-ready, it is time to explore the various options for its
deployment.
In this chapter, we shall discuss the following topics related to the
deployment of a FastAPI app:
• Hypercorn
• Daphne
• Gunicorn
• Render cloud
• Docker
• Google Cloud Platform
• Deta cloud
Hypercorn
Uvicorn, the ASGI server, doesn’t support the HTTP/2 protocol. On the
other hand, Hypercorn supports HTTP/2 and HTTP/1 specifications, in
addition to WebSocket (over both HTTP/1 and HTTP/2). With the use
of the aioquic library, there is also an experimental support for HTTP/3
specifications.
HTTP/2 offers better efficiency over HTTP/1 because of several
improvements. First, it uses the binary transfer protocol. Again, it
employs a multiplexing technique to send multiple streams of data at
once over a single TCP connection. For example, if the client requests
for an index.html file (which internally uses an image file, say logo.png,
and a stylesheet style.css), all the three resources are sent over a single
connection rather than three as is the case in HTTP/1.
Table 10-1 summarizes the difference between HTTP/1 and HTTP/2.
274
Chapter 10 Deployment
HTTPS
One of the important considerations while deploying an API is to ensure
that the server accepts only secure requests from the client. Although
HTTPS uses the same URI scheme as does HTTP, it indicates to the client
browser that it should use an added encryption layer to protect the traffic.
For HTTPS, the server needs to have a certificate. We can create a self-
signed certificate using the RSA cryptography algorithm.
275
Chapter 10 Deployment
Open the Git Bash terminal and enter the following command:
276
Chapter 10 Deployment
Note that the URL where the app is running uses the HTTPS scheme.
Use the same key and certificate files with Hypercorn in the following
command line to enable the HTTPS scheme:
Daphne
Another ASGI implementation used widely for deploying a FastAPI app
is Daphne. It was originally developed to power Django Channels – a
wrapper around the Django framework to enable ASGI support.
277
Chapter 10 Deployment
daphne -e ssl:8000:privateKey=privatekey.
key:certKey=certificate.pem main:app
Gunicorn
With Uvicorn, the FastAPI app is run as a single process. However, in
the production environment, you would like to have some replication of
processes. If there are more clients, the single process can’t handle them
even if the server’s machine has multiple cores. It is possible to have
multiple processes running at the same time and distribute the incoming
requests among them. Such multiple processes of a single API are called
workers.
Gunicorn is another HTTP application server. Although it is a
WSGI-compliant server (which means, by itself, it is not compatible with
FastAPI), its process manager class allows the user to choose which worker
process class to use.
278
Chapter 10 Deployment
279
Chapter 10 Deployment
Next up, connect your GitHub repository so that Render fetches its
contents (Figure 10-5).
280
Chapter 10 Deployment
Environment: Python 3
The runtime environment for your web service
Build command: pip install -r
A script that installs the required libraries requirements.txt
Start command: uvicorn main:app –host
0.0.0.0. –port 10000
281
Chapter 10 Deployment
Docker
Instead of employing the preceding method for deployment, where the
code is put on the cloud platform, and invoking the application server
from there, developers prefer the approach of building a container image
consisting of all the dependencies of the app. The container image is then
deployed either on a server machine or on the cloud platform.
Containers are lightweight as compared to virtual machines (VMs).
They are easily portable. You can build a container on your local machine
and deploy it to any environment.
Docker is one of the most popular platforms for developing,
distributing, and running applications. Building containers with Docker
gives many advantages including security, replicability, simplicity, etc.
The Docker platform consists of Docker Engine that works on top of
the host operating system, and more than one container can be built with
it (Figure 10-7).
282
Chapter 10 Deployment
FROM python:3.10
RUN pip3 install -r requirements.txt
copy ./app app
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--
port", "80"]
283
Chapter 10 Deployment
All the images built can be viewed in the Docker Desktop GUI
(Figure 10-8).
The triangle icon under the Action column indicates that you can run
the image right from inside the application (Figure 10-9).
284
Chapter 10 Deployment
You can now deploy the Docker image on various platforms such as
Heroku, AWS, Google Cloud, etc.
The first step is to put the FastAPI app code in a GitHub repository,
as we had done while deploying on Render. Be sure to include the
requirements.txt file in the repository.
Just as you need a Dockerfile to build the Docker image, GCP needs a
text file named app.yaml in the repository.
YAML is a human-readable data serialization language. Various
application settings such as runtime and version numbers are placed in it.
Save the following script as app.yaml and put it in the GitHub
repository:
runtime: python37
entrypoint: gunicorn -w 4 -k uvicorn.workers.UvicornWorker
main:app
286
Chapter 10 Deployment
To perform the rest of the activity, open the Cloud Shell by clicking the
button, found toward the right side of the Google Cloud dashboard.
The next step is to clone the GitHub repository (in which you have put
the application code, requirements.txt and app.yaml files). Here is an
example (obviously, you will use the URL of your GitHub repository here):
Initially, you run the application as if you have been running so far on
your local machine. To do so, create the virtual environment, install the
libraries from requirements.txt, and run the application.
Execute the following commands from within the Cloud Shell itself:
287
Chapter 10 Deployment
The log emitted on the console should be familiar as you have seen it
while running the code examples in this book:
288
Chapter 10 Deployment
The final step is to deploy the project. Google Cloud uses the YAML
file available in the project directory. It will display the target URL before
asking for confirmation:
Services to deploy:
descriptor: [/home/mlathkar/fastapi/app.yaml]
source: [/home/mlathkar/fastapi]
target project: [fastapi-app-test]
target service: [default]
target version: [20230109t041024]
target url: [https://fastapi-app-test.et.r
.appspot.com]
target service account: [App Engine default service account]
Do you want to continue? (y/n)
Deta Cloud
In the last section of this chapter, we shall introduce another cloud
hosting service, Deta (https://deta.sh). Importantly, hosting your app
on Deta is free (at least for now, although it claims that it will be forever!).
Incidentally, Deta is one of the sponsors of FastAPI.
Deta offers the following products:
Deta Base is a fast, scalable, and secure NoSQL database. It can be
used in serverless applications, while prototyping an application, in
stateful integrations, and more.
Deta Micros (short for microservers) are web apps running at a
specific HTTP endpoint. At the moment, you can build apps based on
Node JS and Python.
289
Chapter 10 Deployment
Deta Drive is a file hosting service, with 10GB storage limit per Deta
account. For instance, you may want to build an image server with the
FastAPI/Python app as the front end and Deta Drive to store the images.
As always, you must sign up on the Deta platform with a username and
password of your choice along with a user email which you need to verify
to use the services.
Make sure that you have an error-free code for a FastAPI app in a
folder which has the Python virtual environment installed. Also, save a
requirements.txt file in the same folder.
To work with the Deta products, you must install the Deta CLI
(command-line interface), as it is through this CLI that all the operations
are done.
You can install the Deta CLI on Mac, Linux, and Windows. For Linux/
Mac, execute the following command from the terminal:
This command downloads and installs the binary and adds deta in the
system path.
After successful installation, run the deta login command from the
PowerShell/Linux terminal. You will be redirected to the Deta sign-in page.
After your account credentials are verified, the control comes back to the
terminal.
290
Chapter 10 Deployment
The deta new command builds the micro. Since ours is a Python app,
specify it in the command line along with the name of the app:
Your app is now available at the endpoint URL. Visit the same in your
browser to obtain a response from the app.
By default, the HTTP authentication is disabled. To enable it, generate
the access token from your account’s settings (Figure 10-11). The token is
valid for 365 days.
291
Chapter 10 Deployment
292
Chapter 10 Deployment
Summary
This is the last chapter of this book. In this chapter, you learned how to use
different tools to deploy your FastAPI app. Alternative ASGI servers have
been explained. The procedure of deploying the app on Render, GCP, and
the Deta cloud has been described in a simple and step-by-step manner.
Following these, you should be easily able to deploy your own app.
293
Index
A main app code, 191
main.py script, 187, 188
Accept-language header, 130, 131
REST operations, 185
Access token, 250, 251, 259,
router package, 189–191
291, 292
@app.delete(), 31, 163
add_book() function, 148, 155, 160,
@app.get() decorator, 31, 63, 95, 98,
167, 174, 269
140, 161
add_header() function, 207
Application object, 30–33, 46, 185,
addnew() function, 67–69,
187, 191, 233, 240, 265, 271
73–75, 83, 260
Application Programming Interface
aiosqlite module, 154–156, 164,
(API), 248
173, 179
analogy, 14
albums/albums.py module, 192
company’s application, 15
albums.py script, 186, 187
electromechanical peripheral
anyio, 25
devices, 14
AnyUrl type, 82
social media services, 16
API documentation, 8, 36, 42,
stand-alone application, 14
54, 57, 62
user registration and
API documentation tools, 36, 64
authentication, 15
APIRouter
“application/schema+json”, 36, 44
albums module, 190
@app.post() decorator, 31, 67, 118,
albums.py script, 186, 187
126, 160
albums routes, 189
@app.put(), 31, 162
books API routes, 188
@app.websocket() decorator, 224
books router, 186
ASCII characters, 66
fastapi module, 185
asdict() and astuple() methods, 71
individual, 185
asgiref package, 23
include_router() method, 187
296
INDEX
297
INDEX
298
INDEX
299
INDEX
300
INDEX
301
INDEX
302
INDEX
303
INDEX
Path operation function, 32, 41, Pydantic models, 21, 23, 64, 87, 91,
156, 159, 160, 166, 174, 178, 101, 121, 122, 147, 148,
188, 194–196, 201, 207, 216, 159–161, 174, 251
240, 246, 248, 252 parameter, 73–75
Path parameters structure, 78
decorator, 46 Pydantic’s built-in validation, 84
employee, 46 Pydantic types, 83
handler function, 46 PyMongo
IP address:port, 46 add_book() function, 174
operation decorator, 46 @app.get() decorator, 176
request URL, 46 BSON representation, 175
type hints, 47 Collection object, 174
type parsing, 48, 49 document, 170
pip3 install jinja2 get_book() function, 176
command, 97 GET /books request, 176
Placeholder identifier, 62, 67 GET operation, 176, 177
PlainTextResponse, 94 insert_one() method, 174
POST Curl command, 68 localhost, 171
Postman app, 66 MongoClient class, 173
POST method, 19, 20, 66, 68, 74 MongoDB Compass, 171–173
POST operation function, 31, 67, MongoDB Query Language, 171
74, 80, 147 MongoDB shell, 171
POST request, 20, 21, 66, 67, 88, PIP utility, 173
117, 122, 269 POST operation, 174
prod_alchemy, 79 Pydantic model, 174
Product objects, 74 schemaless, 170
ProductORM model, 79 start MongoDB server, 170
Products model, 87–89 Python, 213, 231, 232
ProductVal model, 122, 123 application frameworks, 13
profile.html, 114 built-in data types, 80
PUT method, 20, 21 dictionary object, 102
PUT request, 21 dynamic typing, 2
Pydantic library, 70, 71, 82 function, 3
Pydantic fields, 80, 81, 91 IDEs, 3, 4
304
INDEX
305
INDEX
306
INDEX
307
INDEX
T U
Table class methods, 166, 167 UJSONResponse, 136
Template engine, 96–97, 101, 107 Unauthorized user, 15
Template inheritance, 97 Uniform interface, 17, 19
TemplateResponse() Uniform Resource Identifier (URI),
method, 99, 100 17, 19, 157, 212, 275
TestClient object, 260, 261, 263, 264 update_book() function, 152, 156,
Testing 162, 169
AsyncClient, 271 update() method, 167, 169
databases url_for() function, 108, 111, 113
live database, 265 URL validation, 85
override dependency, User-defined exception, 246
265, 266 @app.exception_handler(), 246
override get_db(), 266, 404 error code, 247
268, 269 MyException class, 245
setting up, 268 normal path operation, 247
GET operation function, 260 status code 406, 247
HTTPX client library, 260 Uvicorn, 12, 21, 23, 24, 274, 276, 279
__init__.py file, 261 package, 13, 24
POST operation function, server, 101, 110, 218
260, 262 uvicorn.run() function, 13
PyTest, 260
run, 262
test_list() function, 261, 262 V
test_main.py file, 261 Validation, 82–85
WebSocket, 263, 264 @validator decorator, 85, 86
test_list() function, 261, 262 Virtual machines (VMs), 282
Top-level domain (TLD), 82 VS Code editor, 5
Traditional type, 93
TypeError exception, 3
Type hints, 4–6, 47
W
Type parsing, 48, 49 watchfiles package, 26
typing-extensions module, 26 Web API, 16, 17, 25, 29, 226
308
INDEX
309