Practical Introduction to FastAPI
Introduction
What is FastAPI?
FastAPI is a relatively new Python framework with rich features for building modern high-performance and production-ready web applications in an easy and intuitive manner. I have been building applications with FastAPI recently and I am particularly excited about this framework because it is an ideal and ‘faster’ alternative to Flask or Django for quickly architecting Web applications, APIs, or microservices. FastAPI was created by Sebastián Ramírez.
Perks of using FastAPI
- FastAPI is based on Starlette which makes it incredibly FAST, and on par with Node.js and Go as stated in the official documentation.
- FastAPI supports robust data validation for APIs using Pydantic and Python type hints. This provides validation for standard Python data types (str, int, bool, list, dict, etc.) and other complex data types. It also provides automatic error handling for invalid data types.
- FastAPI automatically generates an interactive documentation based on OpenAI/Swagger UI standard and Redoc alternatively. You do not need to write your own OpenAPI Schemas from scratch or go through the labor of setting it up, isn’t this amazing? 😋
- FastAPI has a very comprehensive documentation and a fast-growing community of developers adopting this framework. Most of the information in this introductory article was gleaned from the documentation. It currently has 25.7k stars on Github 🤩.
You should check out the Official Documentation to fully understand the extra features that FastAPI provides.
What can you build with FastAPI?
- High-performance web applications, REST APIs, and microservices leveraging Databases (i.e supports SQL and NoSQL databases) with security and authentication features,
- Machine learning services are useful for deploying Machine learning-powered applications i.e. for Computer vision, NLP APIs, Chatbots, etc.,
- Real-time chat applications leveraging the support for WebSockets.
4 And any other amazing things you can think of also 😁.
Prerequisites for learning FastAPI the right way
These are pre-requisites I think would facilitate your learning of FastAPI the right way, this is not definitive, however.
PS:- You only need a basic grasp of the underlying concepts outlined in these prerequisites.
- Installed Python 3.6+ on your system
- Knowledge of Python 3.6+(Data types and operations, exceptions, functions, modules, OOP, pip, etc.)
- Understanding of REST API concepts:- request and response lifecycle, anatomy of a request, HTTP request methods (GET, POST, PUT, DELETE, PATCH), etc. Check out this comprehensive blog to get started HERE,
- An IDE or code editor i.e. VSCode, Pycharm, Spyder, etc.
- And most essentially, the will to learn and explore 😃.
Getting Started
I presume you have checked out the pre-requisites and read through the documentation for a better learning experience.
We will be building a digital music inventory REST API with CRUD functionality to understand some fundamental FastAPI concepts. This tutorial has also been structured into steps for a better reading experience.
Let’s go 🚀🚀.
- Setting up a virtual environment
We shall set up a virtual environment to better manage project dependencies. I am using a Windows OS and VSCode specifically for this article. Check out the official documentation on setting up Virtual environments in Python. Navigate to your project directory’s terminal and type:-
$ python -m venv env
$ env/Scripts/activate
This creates a virtual environment named ‘env’ and activates it.
For Ubuntu/Linux users:-
$ python3 -m venv env
$ source env/bin/activate
2. Installing Requirements and dependencies
These are the base dependencies for building this FastAPI basic project:-
- fastapi
- pydantic (For Data validation)
- uvicorn (ASGI server for running FastAPI in local server or production).
$ pip install fastapi pydantic uvicorn uuid
This installs the latest versions of these packages and their dependencies.
3. Building the Music inventory API
3.0 Brief description of the API
We will be building a simple Music inventory API that can enable artists to catalog their songs. Users can create new songs, update created songs, view all their created songs, view all available songs, and delete a created song.
3.1 Getting started (Basics)
Your directory structure should be similar to this:-
📦root
┣ 📂env/
┣ 📜__init__.py
┣ 📜main.py
┗ 📜schemas.py
‘root’ is your root folder, main.py will be used to run the FastAPI app and schemas.py will contain Pydantic schemas for request and response models.
Write the following code in main.py to run your first FastAPI program via the uvicorn server, this basically returns {“message”: “Hello World, I am Similoluwa”} in the browser or any other client.
Amazing, to run this code, simply type:-
$ python main.py
This runs and hosts the server using uvicorn on localhost port 8000 i.e. http://localhost:8000 as specified in the uvicorn.run() method. You can easily view the response from there.
To view the automatically generated OpenAPI docs:- Go to http://localhost:8000/docs
You can easily see the docs and execute any request. On executing the GET request on this path, it simply returns {“message”:” Hello World, I am Similoluwa”} as defined. The docs also get automatically updated after any changes.
3.2 Using Path and Query parameters
What if you wanted to be able to pass in the name from the URI, without hard-coding it directly in your code? You can do this directly using path and query parameters.
Path parameters are extra parameters that can be added to the base URL (i.e http://localhost:8000) and used in the function. Path parameters can be declared in the same format used to declare formatted strings in Python. For example to declare a path parameter “name” and return back a greeting based on that name:-
So when you re-run the server, and checkout http://localhost:8000/michael in your browser, It returns :-
{"message":"Hello World, I am michael"}
The default type of a path parameter is a string, however, you can also declare the type of the path parameter. This can help in data validation if you want to pass in other data types i.e. int, dict as path parameters. Let’s assume you want to read an age (which is an integer) from the path parameter:-
If you check the same http://localhost:8000/similoluwa in your browser, it returns a validation error because the path parameter ‘similoluwa’ is not a valid integer:-
{
"detail":[{"loc":["path","age"],"msg":"value is not a valid integer","type":"type_error.integer"}]
}
Conversely, if you pass in a valid integer as the path parameter i.e. http://localhost:8000/19, it successfully returns the defined response (Notice that there has been some type parsing on the response):-
{"message":"Hello World, I am 19 years old."}
A query based on the anatomy of an endpoint is a key/value pair that comes after the base URL i.e. https://google.com/?search=cow, The ?search=cow suffix is a query where the search is the key and cow is the value.
Query parameters in FastAPI are similar to path parameters, however, they are passed in the function directly. For instance, let’s assume you want to read in the name and age from query parameters i.e. http://localhost:8000/?name=michael&age=10 (Note:- Multiple query parameters are joined by &):-
If you check http://localhost:8000/?name=michael&age=10, it returns a successful response. It also returns a validation error if the passed data types do not match.
{"message":"Hello World, I am michael and I am 10 years old."}
3.3 Using Request and Response Models
Knowledge of request and response models in very essential for developing FastAPI applications. Request models help to define data validation for incoming data i.e. in POST Requests using pydantic primarily while Response models help to validate the data being returned. Request models and response models are also used in generating the schemas for the interactive OpenAPI documentation.
3.3.1 Defining Request Models using Pydantic
Let’s create a Request model based on the schema for the music inventory API (It is a best practice to come up with a high-level design of the schema for any model before implementation). The schema is basically the structure or blueprint with which data is organized.
We shall use pydantic to create this request model, and the pydantic models will be created in the schemas.py file for better modularity.
The **schemas.py** file should contain a declaration of validation, data type, constraints, and default values for the request body model(s).
- The schema is defined in the SongSchema class which inherits from the pydantic BaseModel. The declaration follows this format:-
field_name : Data_Type = default_value
For instance, song_title is validated using data-type str and if an integer or any invalid data-type is passed, it is either type-cast to str or an Exception is returned. Because a default_value for song_title is not declared, song_title is required. For the artiste_name where a default_value is = None, artiste_name is not required.
- The constr(max_length = 100) function used for song_title, and description is used to define constraints. That implies that song_title and description must not be longer than 100 and 300 characters respectively.
- The GenreEnum class is used to define a custom Enum model for selecting a genre for the music. Enum is used when you want the field from the request to be within members in the enumerator. Hence, the genre must be either “hip-hop, afro-pop, rock, blues, others” or an Exception is raised.
- The @validator decorator is used to define custom validation for fields in the pydantic model. This @validator schema is used to check if the release_year is a valid year using Regex i.e. contains four digits, and if the year is greater than the current year. It raises an Exception if the year is invalid or if the year is greater than the current year.
Now, Let’s use this request model in the main API. In the main.py file, import the SongSchema class from the schemas.py file. Define a list to store the music inventory for now.
NOTE :- The list should be replaced by a Database ideally to persist the data, and we shall discuss how to use databases (SQL, NoSQL) in subsequent tutorials.
3.3.2 Creating the Endpoints for CRUD (Create, Read, Update, Delete)
Awesome progress so far 😍. We want to create the endpoints for
- CREATE (Adding a new song),
- READ (Viewing all songs, Viewing song detail for a particular music_id),
- UPDATE (Update Music for a particular music_id),
- DELETE (Delete Music for a particular music_id).
→ For the Create endpoint to add new music:-
- The CREATE endpoint uses a POST request to send data and save it to the music_inventory (our pseudo-database) list via appending. It then returns a dict. The new_song: SongSchema defines that the MusicSchema model should be used as the request body’s model for this endpoint.
The response_model is the data-type of the response object, an Exception will be raised if the response object does not match the data-type specified in the response_model. For instance, the response_model is dict here because a dict is returned.
→ For the READ endpoint to fetch all the music objects in the inventory and fetch music objects by music_id:-
- The GET request to “/” basically returns the list of all created songs which have been stored in the music_inventory pseudo-database. The response_model is List[SongSchema] because it returns a list of SongSchema objects.
- The GET request to “/{music_id}” filters the music_inventory list and returns a list of music objects in the list whose id is equal to the music_id passed in the path parameter. If no music is found, in that case the length of the filtered list is 0, it raises a 404 — Not found Exception.
- Note that the SongSchema object from pydantic has to be type-casted to a dictionary.
→ For the UPDATE and DELETE endpoint to update a created music object:-
- The PUT request to “/{music_id}” takes in the SongSchema from the request body. It filters the music_inventory list based on that music_id, raises an Exception if no song that matched that music_id is found, and places the new SongSchema object from the request body into the index of the former one in the music_inventory list.
- The DELETE request to “/{music_id}” does the same as the PUT request, the only contrast is that it removes the filtered song object from the music_inventory list using:-
music_inventory.pop(idx)
The Complete Code is shown here:-
4. Testing the API
The implemented API endpoints can be tested via the docs at http://localhost:8000/docs or using another REST API client i.e. POSTMAN.
The OpenAPI docs documents all the endpoints and they can be easily tested directly from here. You can fill in the request body values and click on execute to send the request and get a response.
5. Improving this API
- In the next part of this series, we shall discuss how to connect FastAPI to a database to persist data leveraging ORMs to easily perform filtering, reading, deleting operations, and other advanced queries.
CONCLUSION
What you have learnt:-
- How to use Path and query parameters,
- How to create Request models using pydantic for simple and complex schemas,
- How to write a simple CRUD API.
- You have also probably gained motivation to explore FastAPI more.
REFERENCES
- FastAPI official documentation:- https://fastapi.tiangolo.com/
Thank you for reading friend 💗, I hope you learnt something new.
The codes for this tutorial are available on Github HERE