Developing, Testing, and Deploying FastAPI applications
Learn how to develop API services with Python and FastAPI!
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
.
Pressing POST
, we see the following.
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!
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.
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!
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!