An Introduction to FastAPI

An Introduction to FastAPI

I’ve been a Flask developer for a good chunk of time now. I like Flask; it’s fast enough, easy enough to understand and kinda just does what you need it to do. FastAPI came across my radar a couple weeks ago, though, and after trying it out, I don’t know that I’m going to go back to Flask any time soon.

We’re going to walk through a quick “To-Do List” tutorial with FastAPI to explain how to develop a simple CRUD backend with Fast, and then I’ll explain why I like it more than Flask. If you’re looking for a speed benchmark comparison, you can read my blogpost that walks through my findings from a simple benchmarking, but the TL;DR is that FastAPI is a fair bit faster than Flask. This article is going to focus a bit more on coding conventions and much more subjective measures.

FastAPI Introduction - Creating the Backend for a To-Do List App

I know, we’re all tired of the barrage of To-Do List app tutorials. Frankly I get tired of them as well. That said, it’s a solid application to learn a new backend framework, since it’s an arbitrary but usually complete method of learning a backend framework.

We’re going to keep it simple with this one. I’m not going to use a database, the CRUD (Create, Read, Update and Delete) operations will just happen on a JSON file. We won’t do too much else aside from the very basics of reading from and writing to a JSON file so that we can display the basics of using the FastAPI framework.

Let’s start by getting things initialized.

# FastAPI Code
import hashlib
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
import json
from typing import Union

from pydantic import BaseModel

app = FastAPI()

origins = [
    "<http://localhost:3000>"
]

app.add_middleware(
    CORSMiddleware,
    allow_origins=origins
)

We will go into the imports as we use them, but I did want to point out something that I do like with FastAPI over Flask. We create a list of origins (which we could replace with * if we wanted to, to allow any origin) and pass that in to the add_middleware function associated with the FastAPI app object. This, to me, just seems cleaner and more clear than the Flask version below.

# Flask code
app = Flask(__name__)
cors = CORS(app, resources={r"/api/*": {"origins": "*"}})

@app.route("/api/v1/users")
def list_users():
  return "user example"

This is really just a vibe and not super important, but that sort of cleaner vibe kinda follows throughout the rest of the things I like about Fast. The FastAPI code just looks cleaner compared to Flask’s version. You’ll see the same below once we create some routes, but first, let’s talk about a less fuzzy reason I love Fast: strict typing.

Now, this is going to cause some flame bait, and I’ll also admit that you can probably do strict typing with Flask code as well, but the way that Fast handles extracting data from requests, the way they handle dealing with erroneous requests and the way that they implement strict typing really makes for a great development experience. It also allows you to create some awesome docs, which we will get to later.

For illustration in this app, I created a Todo type via a Python class that we will use to ensure the user is giving us data in the format we want. I implemented this essentially like a struct since it just has two member properties, but you can also implement member methods to do things like converting from an object to a dictionary.

#FastAPI code
class Todo(BaseModel):
    todo:Union[str, None]
    id: Union[str, None]

Let’s walk through this. The class inherits from the BaseModel class which we imported above. It also makes use of the Union type from the typing library, which allows us to define variables as being optional. The todo and id variables can either be a string type or a None type, so I can create a Todo object with just an id or just a todo member variable.

Now let’s get to routes. We can start simple with a “hello world” route:

#FastAPI code
@app.get('/hello_world')
def hello_world():
    return {'status_code':200, 'message':'hello world!'}

The route method is attached to the route decorator itself, instead of the odd way that you do it in Flask with a methods array in the route decorator call.

#Flask code
@app.route('/hello_world',methods=['GET'])
def hello_world():
	return {'status_code':200, 'message':'hello world!'}

Again, this is more or less nitpicky (the reasons I go through in a second are less nitpicky) but the general cleanliness of FastAPI’s code kinda sticks with me.

Now, the bigger reason I like Fast code more than Flask has to do with how it extracts data from a request, in that it basically tries to abstract that away from you as much as possible. Check out these examples and I’ll explain what I mean.

#FastAPI code
datadir = './data/mydata.json'
@app.post('/name')
def set_name(name:str):
    with open(datadir, 'r') as data:
        js = json.loads(data.read())
    with open(datadir, 'w') as data:
        try:
            js['name'] = name
            print(f'[-] JS: {str(js)}')
            data.write(json.dumps(js))
            return {'status':200, 'message':'Wrote name to file!','name':name}
        except Exception as e:
            print(f'[x] Exception: {str(e)}')
            return {'status':400, 'message':'Error writing name to file!'}

If you look at the code above, at no point do you see code where you have to figure out where on the request object the JSON is, how it’s structured, how you find the data, etc. FastAPI just kinda handles that: the request you get should be an object that contains a ‘name’ property that should correspond to a string. If it doesn’t, the route will return an error without ever entering the function. As long as it does, you access it by just listing the name variable in the inputs to the function.

This is so much cleaner than the corresponding Flask code to extract data out of the POST body. I can just assume that the data was extracted correctly and move on dumping the data to the JSON file.

GET requests are just as easy, really. If I were to instead want to send a GET request to the above function, I could just change the method in the decorator and send the request to /name?name=Mitch and I’m good to go. It would extract the name data from the URL argument instead of the POST body.

Really those are the biggest differences I’ve found, aside from the matter of speed, which I discussed in the video below.

I’m fairly certain that from now on I’ll be developing my Python backends in FastAPI instead of Flask. I just like the cleanliness of the code and relative simplicity compared to Flask, and the speed definitely helps as well.