FastAPI : Create your own API endpoint - Part 1

Background

Ever since I can remember, I've been fascinated with the idea of developing an IoT device capable of responding to my inquiries whenever I seek specific information. Initially, I was under the impression that establishing an API endpoint or server would be a highly complex task, possibly beyond my current skill set. However, I've discovered that this assumption was misplaced. The process is not as daunting as I had imagined.

Introducing FastAPI

FastAPI, a project we truly admire for its open-source nature, offers the flexibility to operate a REST API server and customize endpoints to your heart's content. The possibilities are limitless: you can directly access details from the running hardware, execute scripts in any language of your choice, and receive results in JSON format. It encompasses the most widely used methods in RESTful API design, including GET, POST, PUT, DELETE, PATCH, OPTIONS, and HEAD. What's even more impressive is its ability to automatically generate comprehensive documentation, presented in a visually appealing Swagger interface!

Ok ok wait, let's rewind !

FastAPI is a framework running in Python, so, you need an OS that can run Python and pip. Let's assume we are running on Raspberry OS in our IoT example : 

$ sudo apt install pip
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
Note, selecting 'python3-pip' instead of 'pip'
[...]
Setting up python3-dev (3.11.2-1+b1) ...
Processing triggers for man-db (2.11.2-2) ...

Let's create a virtual environment where our FastAPI deployment will run without affecting other users in our platform. Using a virtual environment is a recommended practice as it keeps your project dependencies isolated from the system packages:

$ sudo apt install python3.11-venv
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
The following additional packages will be installed:
  python3-pip-whl python3-setuptools-whl
The following NEW packages will be installed:
  python3-pip-whl python3-setuptools-whl python3.11-venv
0 upgraded, 3 newly installed, 0 to remove and 0 not upgraded.
Need to get 2835 kB of archives.
[...]
Setting up python3-setuptools-whl (66.1.1-1) ...
Setting up python3-pip-whl (23.0.1+dfsg-1) ...
Setting up python3.11-venv (3.11.2-6) ...

$ python3 -m venv FastAPIenv
$ source FastAPIenv/bin/activate
(FastAPIenv) pi@pi5:~ $

Now, we can start FastAPI deployment : 

pip install fastapi uvicorn

Collecting fastapi
  Downloading fastapi-0.104.1-py3-none-any.whl (92 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 92.9/92.9 kB 1.7 MB/s eta 0:00:00
Collecting uvicorn
  Downloading uvicorn-0.24.0.post1-py3-none-any.whl (59 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 59.7/59.7 kB 2.8 MB/s eta 0:00:00
Collecting anyio<4.0.0,>=3.7.1
[...]
1.8/1.8 MB 3.6 MB/s eta 0:00:00
Installing collected packages: typing-extensions, sniffio, idna, h11, click, annotated-types, uvicorn, pydantic-core, anyio, starlette, pydantic, fastapi
Successfully installed annotated-types-0.6.0 anyio-3.7.1 click-8.1.7 fastapi-0.104.1 h11-0.14.0 idna-3.5 pydantic-2.5.2 pydantic-core-2.14.5 sniffio-1.3.0 starlette-0.27.0 typing-extensions-4.8.0 uvicorn-0.24.0.post1
$

Let's create a sample FastAPI data model. Go to your virtual environment root path and edit a myApp.py file : 

$ cd FastAPIenv/
$ vi myApp.py

Add the following content into the file : 

from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def read_root():
return {"Hello": "World"}

@app.get("/items/{item_id}")
def read_item(item_id: int, q: str = None):
return {"item_id": item_id, "q": q}

IMPORTANT : make sure you are intending with <tab> and not <space>. Otherwise, you will have funny error messages. If you are not cautious enough with this, you will receive the following error in journalctl : 

Nov 26 23:12:11 pi5 uvicorn[3626]: TabError: inconsistent use of tabs and spaces in indentation
Nov 26 23:12:11 pi5 systemd[1]: fastapi.service: Main process exited, code=exited, status=1/FAILURE
Nov 26 23:12:11 pi5 systemd[1]: fastapi.service: Failed with result 'exit-code'.

Let's start the FastAPI server now : 

$ uvicorn myApp:app --host 0.0.0.0 --port 8000
INFO:     Started server process [1479]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)

In the above command line, myApp is the python file without extension and the app part is the name of the FastAPI instance name defined inside the python file itself.

To test if the above works, simply go to this URL (replace with your pi IP address) :

http://<pi_IP>:8000/

The result should be the famous "Hello World!" displayed as a JSON string : 

{"Hello":"World"}

You should see the access log displayed in the terminal window. If you understand what we've done, we only started FastAPI in interactive mode. If I quit the shell window, I will actually kill the server. Let's create a service !

$ sudo vi /etc/systemd/system/fastapi.service

Add the following content to the file (make sure to adjust any path or name you may have changed) :

[Unit]
Description=FastAPI server
After=network.target

[Service]
User=pi
WorkingDirectory=/home/pi/FastAPIenv
ExecStart=/home/pi/FastAPIenv/bin/uvicorn myApp:app --host 0.0.0.0 --port 8000
Restart=always
# Other configurations you might want to add:
# Environment variables
# Environment="VARIABLE=VALUE"
# Timeout settings
# TimeoutStartSec=10
# Specify a logfile
# StandardOutput=append:/var/log/fastapi.log
# StandardError=append:/var/log/fastapi.log

[Install]
WantedBy=multi-user.target

$ sudo systemctl daemon-reload
$ sudo systemctl enable fastapi.service
Created symlink /etc/systemd/system/multi-user.target.wants/fastapi.service → /etc/systemd/system/fastapi.service.
sudo systemctl start fastapi.service
$ sudo systemctl status fastapi.service
● fastapi.service - FastAPI server
     Loaded: loaded (/etc/systemd/system/fastapi.service; enabled; preset: enabled)
     Active: active (running) since Fri 2023-11-24 23:33:13 CET; 4s ago
   Main PID: 1635 (uvicorn)
      Tasks: 1 (limit: 4453)
        CPU: 672ms
     CGroup: /system.slice/fastapi.service
             └─1635 /home/pi/FastAPIenv/bin/python3 /home/pi/FastAPIenv/bin/uvicorn myApp:app --host 0.0.0.0 --port 8000

Nov 24 23:33:13 pi5 systemd[1]: Started fastapi.service - FastAPI server.
Nov 24 23:33:14 pi5 uvicorn[1635]: INFO:     Started server process [1635]
Nov 24 23:33:14 pi5 uvicorn[1635]: INFO:     Waiting for application startup.
Nov 24 23:33:14 pi5 uvicorn[1635]: INFO:     Application startup complete.
Nov 24 23:33:14 pi5 uvicorn[1635]: INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)

At this stage, we have FastAPI starting at device boot up sequence and already a first endpoint created. Let's dig further and add some functionalities !

I need to get the following information with my newly created API server : 
  • Uptime of the system
  • CPU temperature
  • Basis system information
Let's modify our myApp.py file and replace it with the below content :  

---
from fastapi import FastAPI
from datetime import timedelta

import platform
import os
import socket

app = FastAPI()

@app.get("/")
def read_root():
return {"Hello": "World"}

@app.get("/uptime")
def get_uptime():
# Read the uptime information from /proc/uptime
with open('/proc/uptime', 'r') as f:
uptime_seconds = float(f.readline().split()[0])

# Convert the uptime from seconds to a timedelta for formatting
uptime_timedelta = timedelta(seconds=uptime_seconds)

    # Format the uptime into a human-readable format
    # For example, "1 day, 3:22:01"
uptime_str = str(uptime_timedelta).split('.')[0]

return {"uptime": uptime_str}

@app.get("/sensors/temperature/cpu")
def get_cpu_temperature():
try:
# Open the file that contains the CPU temperature.
with open("/sys/class/thermal/thermal_zone0/temp", "r") as file:
# Read the file, convert content to an integer, and divide by 1000 to get the temperature in Celsius.
cpu_temp = int(file.read()) / 1000
return {"cpu_temperature": cpu_temp}
except IOError:
return {"error": "Could not read CPU temperature"}

@app.get("/system/info")
def get_system_info():
try:
host_name = socket.gethostname()
host_ip = socket.gethostbyname(host_name)
info = {
"os_version": platform.platform(),
"python_version": platform.python_version(),
"architecture": platform.machine(),
"hostname": platform.node(),
"processor": platform.processor(),
"kernel": platform.uname().release,
"hostname": host_name,
"ip_address": host_ip,
"platform": platform.system(),
"platform-release": platform.release(),
"platform-version": platform.version(),
"architecture": platform.machine()
}
return info
except Exception as e:
return {"error": str(e)}
---

Each time you modify the python code, restart the FastAPI service using sysctl command : 

$ sudo systemctl restart fastapi.service

Now, open your browser to your pi IP and add the following subfolder : 

http://<pi_ip>:8000/uptime

You will get something similar to this : 

{"uptime":"0:57:35"}

Similar behavior for the 2 other endpoints : 

http://<pi_ip>:8000/sensors/temperature/cpu
http://<pi_ip>:8000/system/info

Documentation

You will immediately notice that it might become complex very shortly to have a view on the different endpoints you are creating.

FastAPI is generating the documentation and the test page for you !!!

Browse to your pi address as before but change the subfolder to /doc : 

http://<pi_ip>:8000/docs

You will see something similar to this : 

All the endpoints we have created are actually displayed with the method, the path and if you expand any of them you will have the ability to test it directly from the UI.

Let's see with system info, expand it and click Try Out and then Excecute : 


How cool is this ?

You have the cURL equivalent of the call, the response in a perfectly JSON formatted output, the headers, the returned code ... this is devel Heaven's ;)

I think I will spend some time with FastAPI, I have a couple of projects in mind.


I hope this helps ;)









Comments

What's hot ?

Wallbox : Get The Most Of It (with API)

Mac OS X : Display images in-line with terminal

ShredOS : HDD degaussing with style