Developing, Testing, and Deploying FastAPI applications

Learn how to develop API services with Python and FastAPI!

·

9 min read

What is FastAPI?

FastAPI is an intuitive Python web framework for creating API applications. It is among the fastest web frameworks and one of the easiest - if not the easiest - to learn.

Additionally, FastAPI integrates with asynchronous tasks by default - thus making for faster applications. FastAPI also has default integration with swagger documentation and makes it easy to configure and update.

These are just some of the advantages of using FastAPI; now, let's get our hands dirty in writing the code!

The application we're building

We're going to be building a hash table API. This is so that we can take advantage of Python's dict data structure so we can focus our priority on creating the FastAPI application instead.

Downloading packages

We'll need pytest, fastapi, uvicorn - for the web server -, and deta for this tutorial, so make sure to download them if you haven't already!
pip install pytest, fastapi, uvicorn[standard], deta

Developing our hash table

First lets setup our project with the following folder structure.

├───app   
├───tests
└───main.py

In app create a file called hash_table.py and paste the following code.

class HashTable:
    def __init__(self):
        self.hash_table = {}

    def add(self, key, value):
        self.hash_table[key] = value

    def delete(self, key):
        self.hash_table.pop(key, None)

    def get(self, key):
        return self.hash_table.get(key, None)

Every developer must understand the importance of using tests in applications. In the tests directory, create a new file called test_hash_table.py and paste the following code.

import pytest
import sys 

sys.path.append('..')
from app.hash_table import HashTable

test_hashtable = HashTable()

def test_add():
    test_hashtable.add('key', 'value')
    assert test_hashtable.get('key') == 'value'

def test_replace_word():
    og_value = test_hashtable.get('key')
    test_hashtable.add('key', 'new value')
    assert test_hashtable.get('key') != og_value

def test_get():
    test_hashtable.add('key', 'value')
    assert test_hashtable.get('key') == 'value'

def test_delete():
    test_hashtable.delete('key')
    assert test_hashtable.get('key') is None

In my machine, running pytest from the tests directory will yield the following result: 4 passed in 0.05s. With that out of the way, lets now focus on developing the API!

Developing the API

Alright, it's now time to start writing our FastAPI API! For simplicity's sake, we will put the FastAPI code in the main.py file directly. From the root directory, create a new file called main.py. Our project directory will now look like this.

├───app
│   ├───hash_table.py
├───tests
│   ├───test_hash_table.py
├───main.py

In our main.py file, we first import the FastAPI framework, and our hash_table.py file.

from fastapi import FastAPI
import app.hash_table as HashTable

From here we initialize our API app by calling the FastAPI and HashTable constructors.

hash_table = HashTable()
app = FastAPI()

We can now start writing our API code! The first thing that we'll need is to define the root path. For that path, we'll simply display a greeting message.

@app.get("/")
async def root():
    return {"Greetings": "Welcome to FastAPI!"}

Now run the server by typing uvicorn main:app --reload in the command line. The command refers to the following

main: the file main.py
app: the object created inside of main.py with the line app = FastAPI()
--reload: make the server restart after code changes. Only use for development. Similar to Flutter's hot restart

Now go to the URL provided by the server, http://127.0.0.1:8000. You should see the following.

{"Greetings":"Welcome to FastAPI!"}

So what does the following code do? Lets dissect it line by line.

@app.get("/")

@app is needed for FastAPI commands. The get is an HTTP method, while the "/" is the url path of that specific API request. Below that we call a function that will return something. Here we return a dict object.

Notice that we use the term async; we don't necessarily need to call the function with the async keyword since we don't make use of libraries that communicates with something (a database, an API, the file system, etc.), you can still use just normal def function_name(), but make sure that that function will not communicate with any of the above-mentioned things.

With that out of the way, we now have a template for writing FastAPI endpoints.

@app.http_method("url_path")
async def function():
    return something

We now have a basic understanding of making FastAPI endpoints. Let's now create our first endpoint for our hash table class!

We'll be creating the endpoint for the hash table's add method. Lets first go back to the implementation of that method in the hash table.

# in hash_table.py
def add(self, key, value):
        self.hash_table[key] = value

Here we see that add takes in two values, key, and value. In FastAPI, we can use path parameters in making API endpoints that take in a value. Additionally, since we're adding a new key-value pair to our API, we will make use of a post request.

@app.post("/add/{key}-{value}")
async def add_entry(key, value):
    hash_table.add(key, value)
    return f"Key-value pair {key}-{value} added"

In @app.post("/add/{key}-{value}"), FastAPI will pass down the values of key and value to the function add_entry as the arguments key, and value respectively.

To test this out, we will go to the built in documentation provided by FastAPI http://127.0.0.1:8000/docs.

image.png

Pressing POST, we see the following.

image.png

As you can see, we can easily try out out API endpoints through the built in documentation provided by FastAPI! Try adding a key-value pair to see how it works!

image.png

Pressing Execute will allow us to see the response body. In my case, this would be.

"Key-value pair FastAPI-is Awesome! added"

Lets now create the endpoints for the hash table's delete and get methods respectively.

@app.delete("/delete/{key}")
async def delete_entry(key):
    hash_table.delete(key)
    return f"Key: {key} deleted"

For the HTTP method, we use delete here as we're explicitly removing something from the server.

@app.get("/get/{key}")
async def get_value(key):
    return hash_table.get(key)

Here, on the other hand, we use the HTTP method get as we're merely requesting to see the value associated with the key we provided.

Going back to the built in documentation, you will now see that there are now four endpoints.

image.png

Let's test these endpoints! In the add endpoint, add hashnode, and blog respectively as parameters for key and value then press Execute. You should see the following response.

"Key-value pair hashnode-blog added"

Now in the get endpoint, put in hashnode as the parameter for key then press execute. You should see the following response.

"blog"

Let's now delete this key from the server. Add hashnode as the parameter for key in the delete endpoint then press execute. You should see the following response.

"Key: hashnode deleted"

Now go back to the get end point and search for hashnode. You should see a null response in the response body. This is because we tried to look for a key that doesn't exist in the server.

Here's the full code for our main.py file.

from fastapi import FastAPI
from app.hash_table import HashTable

hash_table = HashTable()
app = FastAPI()

@app.get("/")
async def root():
    return {"Greetings": "Welcome to FastAPI!"}

@app.post("/add/{key}-{value}")
async def add_entry(key, value):
    hash_table.add(key, value)
    return f"Key-value pair {key}-{value} added"

@app.delete("/delete/{key}")
async def delete_entry(key):
    hash_table.delete(key)
    return f"Key: {key} deleted"

@app.get("/get/{key}")
async def get_value(key):
    return hash_table.get(key)

That's all for out API code! Lets now start writing tests for our API!

Testing the API

In the tests directory, create a new file called test_main.py this would contain the tests for our API. FastAPI already provides us a built-in test client. All we need to do is import it.

# in test_main.py
from fastapi.testclient import TestClient

from main import app

client = TestClient(app)

Similarly to writing our endpoint, we'll first write the code to test our root endpoint.

def test_read_main():
    response = client.get("/")
    assert response.status_code == 200
    assert response.json() ==  {"Greetings": "Welcome to FastAPI!"}

Similar to testing non-API applications, we make use of the assert keyword. Here we test that the response we get from the root endpoint is similar to the one we saw earlier in the documentation.

Note that we don't make use of the async keyword here. This allows us to make use of pytest without necesarry complications.

TestClient actually makes use of Starlette under the hood, thus we can use pytest directly with FastAPI. In my machine, running pytest test_main.py in the tests directory will show the following.

1 passed in 0.28s

Let's now test our 2nd endpoint, namely the add endpoint. I'll use FastAPI, and tutorial as parameters for key and value respectively.

def test_post_method_add():
    key = "FastAPI"
    value = "tutorial"
    response = client.post(f"/add/{key}-{value}")
    assert response.status_code == 200
    assert response.json() == f"Key-value pair {key}-{value} added"

Let's now test the get method with FastAPI as the key.

def test_get_method_get():
    key = "FastAPI"
    expected_value = "tutorial"
    response = client.get(f"/get/{key}")
    assert response.status_code == 200
    assert response.json() == expected_value

Let's now write another test for the get method, this time, with a key that does not exist in the hash table. I'll use Flask for the key parameter.

def test_get_method_get2():
    key = "Flask"
    expected_value = None
    response = client.get(f"/get/{key}")
    assert response.status_code == 200
    assert response.json() == expected_value

Lastly, let's write our test for the delete method by deleting the key-value pair we added in the hash table earlier.

def test_delete_method_delete():
    key = "FastAPI"
    expected_value = f"Key: {key} deleted"
    response = client.delete(f"/delete/{key}")
    assert response.status_code == 200
    assert response.json() == expected_value

Let's now run pytest test_main.py again.

5 passed in 0.31s

Here's the full code for our test_main.py file.

from fastapi.testclient import TestClient

from main import app

client = TestClient(app)

def test_read_main():
    response = client.get("/")
    assert response.status_code == 200
    assert response.json() ==  {"Greetings": "Welcome to FastAPI!"}

def test_post_method_add():
    key = "FastAPI"
    value = "tutorial"
    response = client.post(f"/add/{key}-{value}")
    assert response.status_code == 200
    assert response.json() == f"Key-value pair {key}-{value} added"

def test_get_method_get():
    key = "FastAPI"
    expected_value = "tutorial"
    response = client.get(f"/get/{key}")
    assert response.status_code == 200
    assert response.json() == expected_value

def test_get_method_get2():
    key = "Flask"
    expected_value = None
    response = client.get(f"/get/{key}")
    assert response.status_code == 200
    assert response.json() == expected_value

def test_delete_method_delete():
    key = "FastAPI"
    expected_value = f"Key: {key} deleted"
    response = client.delete(f"/delete/{key}")
    assert response.status_code == 200
    assert response.json() == expected_value

We have now finished writing the tests for our API! We can now deploy this API! FastAPI makes this easy yet again by using Deta.

Deploying the API

Deta is one of FastAPI's sponsors so integration between their platform and the FastAPI framework is seamless.

First thing you need to do is to create a free account on Deta.

Once you have a Deta account, you'll need to download its CLI.

On Linux and macOS, the command below installs the Deta CLI.

curl -fsSL https://get.deta.dev/cli.sh | sh

Windows on the other hand has a different command.

iwr https://get.deta.dev/cli.ps1 -useb | iex

Make sure that the Deta CLI is detected by running;

deta --help

Next, login to your account by using the command below;

deta login

This will open a web browser for you to log in to your Deta account.

Now, in the project root directory, create a new file called requirements.txt and put in there fastapi

# requirements.txt
fastapi

Once you've done that, you can easily deploy the app on Deta by running;

deta new

You should see a json similar to this.

{
        "name": "fastapi-tutorial",
        "runtime": "python3.7",
        "endpoint": "https://f2n9x0.deta.dev",
        "visor": "enabled",
        "http_auth": "disabled"
}

Going to the link https://f2n9x0.deta.dev, we see this!

{"Greetings":"Welcome to FastAPI!"}

Navigating to https://f2n9x0.deta.dev/docs, you'll see the documentation!

image.png

We've now completed our application! You can view the full source code here.

Conclusion

Congratulations! You've now learned the basics of developing, testing, and deploying a FastAPI application! If you want to explore FastAPI more, I highly suggest reading through its amazing documentation!

If you've found this useful, consider buying me a coffee! I'd appreciate any support I can get! Feel free to reach out to me on twitter as well!