Create your first REST API in FastAPI
By: Adnan Siddiqi
Tags: Python, Fastapi, Rest Api

Create your first REST API in FastAPI

Photo by Nicolas Hoizey on Unsplash

In this post, I am going to introduce FastAPI: A Python-based framework to create Rest APIs. I will briefly introduce you to some basic features of this framework and then we will create a simple set of APIs for a contact management system. Knowledge of Python is very necessary to use this framework.

Before we discuss the FastAPI framework, let’s talk a bit about REST itself.

From Wikipedia:

Representational state transfer (REST) is a software architectural style that defines a set of constraints to be used for creating Web services. Web services that conform to the REST architectural style, called RESTful Web services, provide interoperability between computer systems on the Internet. RESTful Web services allow the requesting systems to access and manipulate textual representations of Web resources by using a uniform and predefined set of stateless operations. Other kinds of Web services, such as SOAP Web services, expose their own arbitrary sets of operations.[1]

What is the FastAPI framework?

From the official website:

FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints.

Yes, it is fast, very fast and it is due to out of the box support of the async feature of Python 3.6+ this is why it is recommended to use the latest versions of Python.

FastAPI was created by Sebastián Ramírez who was not happy with the existing frameworks like Flask and DRF. More you can learn about it here. Some of the key features mentioned on their website are:

Fast : Very high performance, on par with NodeJS and Go (thanks to Starlette and Pydantic). One of the fastest Python frameworks available.

: Very high performance, on par with and (thanks to Starlette and Pydantic). One of the fastest Python frameworks available. Fast to code : Increase the speed to develop features by about 200% to 300%. *

: Increase the speed to develop features by about 200% to 300%. * Fewer bugs : Reduce about 40% of human (developer) induced errors. *

: Reduce about 40% of human (developer) induced errors. * Intuitive : Great editor support. Completion everywhere. Less time debugging.

: Great editor support. Completion everywhere. Less time debugging. Easy : Designed to be easy to use and learn. Less time reading docs.

: Designed to be easy to use and learn. Less time reading docs. Short : Minimize code duplication. Multiple features from each parameter declaration. Fewer bugs.

: Minimize code duplication. Multiple features from each parameter declaration. Fewer bugs. Robust : Get production-ready code. With automatic interactive documentation.

: Get production-ready code. With automatic interactive documentation. Standards-based: Based on (and fully compatible with) the open standards for APIs(Open API)

The creator of the FastAPI believed in standing on the shoulder of giants and used existing tools and frameworks like Starlette and Pydantic

Installation and Setup

I am going to use Pipenv for setting up the development environment for our APIs. Pipenv makes it easier to isolate your development environment irrespective of what things are installed on your machine. It also lets you pick a different Python version than whatever is installed on your machine. It uses Pipfile to manage all your project-related dependencies. I am not gonna cover Pipenv here in detail so will only be using the commands that are necessary for the project.

You can install Pipenv via PyPy by running pip install pipenv

pipenv install --python 3.9

Once installed you can activate the virtual environment by running the command pipenv shell

You may also run pipenv install --three where three means Python 3.x.

Once installed you can activate the virtual environment by running the command pipenv shell

First, we will install FastAPI by running the following command: pipenv install fastapi

Note it’s pipenv, NOT pip. When you are into the shell you will be using pipenv . Underlying it is using pip but all entries are being stored in Pipfile . The Pipfile will be looking like below:

OK, so we have set up our dev environment. It’s time to start writing our first API endpoint. I am going to create a file called main.py . This will be the entry point of our app.

from fastapi import FastAPI app = FastAPI()

def home():

return {"Hello": "FastAPI"} @app .get("/")def home():return {"Hello": "FastAPI"}

If you have worked on Flask then you will be finding it pretty much similar. After importing the required library you created an app instance and created your first route with a decorator.

Now you wonder how to run it. Well, FastAPI comes with the uvicorn which is an ASGI server. You will simply be running the command uvicorn main:app --reload

You provide the file name(main.py in this case) and the class object(app in this case) and it will initiate the server. I am using the --reload flag so that it reloads itself after every change.

Visit http://localhost:8000/ and you will see the message in JSON format {"Hello":"FastAPI"}

Cool, No?

FastAPI provides an API document engine too. If you visit http://localhost:8000/docs which is using the Swagger UI interface.

Or if you need something fancy then visit http://localhost:8080/redoc

FastAPI also provides an OpenAPI version of API endpoints, like this http://127.0.0.1:8000/openapi.json

Path and Parameters

Let’s move forward. We add another API endpoint. Say, it’s about fetching contact details by its id.

So here is a method, contact_details that accepts only an int parameter and just returns it as it in a dict format. Now when I access it via cURL it looks like below:

Now, what if I pass a string instead of an integer? You will see the below

Did you see it? it returned an error message that you sent the wrong data type. You do not need to write a validator for such petty things. That’s the beauty while working in FaastAPI.

Query String

What if you pass extra data in the form of query strings? For instance your API end-point returns loads of records hence you need pagination. Well, no issue, you can fetch that info as well.

First, we will import the Optional type:

from typing import Optional



def contact_details(contact_id: int, page: Optional[int] = 1):

if page:

return {'contact_id': contact_id, 'page': page}

return {'contact_id': contact_id} @app .get("/contact/{contact_id}")def contact_details(contact_id: int, page: Optional[int] = 1):if page:return {'contact_id': contact_id, 'page': page}return {'contact_id': contact_id}

Here, I passed another parameter, page and set its type Optional[int] here. Optional, well as the name suggests it's an optional parameter. Setting the type int is making sure that it only accepts the integer value otherwise, it'd be throwing an error like it did above.

Access the URL http://127.0.0.1:8000/contact/1?page=5 and you will see something like below:

Cool, No?

So far we just manually returned the dict . It is not cool. It is quite common that you input a single value and return a YUUGE JSON structure. FastAPI provides an elegant way to deal with it, using Pydantic models.

Pydantic models actually help in data validation, what does it mean? It means it makes sure that the data which is being passed is valid, if not otherwise it returns an error. We are already using Python’s type hinting and these data models make that the sanitized data is being passed thru. Let’s write a bit of code. I am again going to extend the contact API for this purpose.

from typing import Optional from fastapi import FastAPI

from pydantic import BaseModel

app = FastAPI() class Contact(BaseModel):

contact_id:int

first_name:str

last_name:str

user_name:str

password:str

async def create_contact(contact: Contact):

return contact @app .post('/contact')async def create_contact(contact: Contact):return contact

I imported the BaseModel class from pydantic. After that, I created a model class that extended the BaseModel class and set 3 fields in it. Do notice I am also setting the type of it. Once it's done I created a POST API endpoint and passed an Contact parameter to it. I am also using async here which converts the simple python function into a coroutine. FastAPI supports it out of the box.

Go to http://localhost:8080/docs and you will see something like below:

When you run the CURL command you would see something like the below:

As expected it just returned the Contact object in JSON format.

As you notice, it just dumps the entire model in JSON format, including password. It does not make sense even if your password is not in plain-text format. So what to do? Response Model is the answer.

What is Response Model

As the name suggests, a Response Model is a model that is used while sending a response against a request. Basically, when you just used a model it just returns all fields. By using a response model you can control what kind of data should be returned back to the user. Let’s change the code a bit.

class Contact(BaseModel):

contact_id:int

first_name:str

last_name:str

user_name:str

password:str class ContactOut(BaseModel):

contact_id:int

first_name:str

last_name:str

user_name:str

def home():

return {"Hello": "FastAPI"} @app .get("/")def home():return {"Hello": "FastAPI"}

async def create_contact(contact: Contact):

return contact @app .post('/contact', response_model=ContactOut)async def create_contact(contact: Contact):return contact

I have added another class, ContactOut which is almost a copy of the Contact class. The only thing which is different here is the absence of the password field. In order to use it, we are going to assign it in the response_model parameter of the post decorator. That's it. Now when I run hit the same URL it will not return the password field.

As you can see, no password field is visible here. If you notice the /docs URL you will see it visible over there as well.

Using a different Response Model is feasible if you are willing to use it in multiple methods but if you just want to omit the confidential information from a single method then you can also use response_model_exclude parameter in the decorator.



async def create_contact(contact: Contact):

return contact @app .post('/contact', response_model=Contact, response_model_exclude={"password"})async def create_contact(contact: Contact):return contact

The output will be similar. You are setting the response_model and response_model_exclude here. The result is the same. You can also attach metadata with your API endpoint.



async def create_contact(contact: Contact):

return contact @app .post('/contact', response_model=Contact, response_model_exclude={"password"},description="Create a single contact")async def create_contact(contact: Contact):return contact

We added the description of this endpoint which you can see in the doc.

FastAPI documentation awesomeness does not end here, it also lets you set the example JSON structure of the model.

class Contact(BaseModel):

contact_id:int

first_name:str

last_name:str

user_name:str

password:str class Config:

schema_extra = {

"example": {

"contact_id": 1,

"first_name": "Jhon",

"last_name": "Doe",

"user_name": "jhon_123",

}

}

And when you do that, it is rendered as:

Error handling in FastAPI

It is always possible that you do not get the required info. FastAPI provides HTTPException class to deal with such situations.



async def contact_details(id: int):

if id < 1:

raise HTTPException(status_code=404, detail="The required contact details not found")

contact = Contact(contact_id=id, first_name='Adnan', last_name='Siddiqi', user_name='adnan1', password='adn34')

return contact @app .get("/contact/{id}", response_model=Contact, response_model_exclude={"password"},description="Fetch a single contact")async def contact_details(id: int):if id < 1:raise HTTPException(status_code=404, detail="The required contact details not found")contact = Contact(contact_id=id, first_name='Adnan', last_name='Siddiqi', user_name='adnan1', password='adn34')return contact

A simple endpoint. It returns contact details based on the id. If the id is less than 1 it returns a 404 error message with details.

Before I leave, let me tell you how you can send custom headers.

from fastapi import FastAPI, HTTPException, Response

description="Fetch a single contact")

async def contact_details(id: int, response: Response):

response.headers["X-LOL"] = "1"

if id < 1:

raise HTTPException(status_code=404, detail="The required contact details not found")

contact = Contact(contact_id=id, first_name='Adnan', last_name='Siddiqi', user_name='adnan1', password='adn34')

return contact @app .get("/contact/{id}", response_model=Contact, response_model_exclude={"password"},description="Fetch a single contact")async def contact_details(id: int, response: Response):response.headers["X-LOL"] = "1"if id < 1:raise HTTPException(status_code=404, detail="The required contact details not found")contact = Contact(contact_id=id, first_name='Adnan', last_name='Siddiqi', user_name='adnan1', password='adn34')return contact

After importing the Response class I passed request parameter of type Request and set the header X-LOL

After running the curl command you will see something like the below:

You can find x-lol among headers. LOL!

Conclusion

So in this post, you learned how you can start using FastAPI for building high-performance APIs. We already have a minimal framework called Flask but FastAPI’s asynchronous support makes it much attractive for modern production systems especially machine learning models that are accessed via REST APIs. I have only scratched the surface of it. You can learn further about it on the official FastAPI website.

Hopefully, in the next post, I will be discussing some advanced topics like integrating with DB, Authentication, and other things.