Part 0 - Let's build our own fastAPI

I recently started using fastAPI for developing APIs around machine learning models. It’s a well designed library and a real joy to work with. It contains all the right features for the space it wants to occupy (quickly building properly validated JSON APIs) and doesn’t overload you with a lot of functionality beyond that use case. And the way it uses the new python 3.5+ typing features to define input/output schemas is quite beautiful.

Here is a little made-up example to spike your curiousity. The meat of the code is the decorated calculate function, which sets up an API endpoint that accepts HTTP POST requests. The 2 classes define the input model to this endpoint.

from typing import Union
from enum import Enum
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class CalculatorOperation(str, Enum):
    add = "add"
	subtract = "subtract"
	multiply = "multiply"
	divide = "divide"


class CalculateIn(BaseModel):
    a: Union[int, float]
    b: Union[int, float]
	operation: CalculatorOperation
    

@app.post("/calculate", response_model=Union[int, float])
def calculate(i: CalculateIn):
    return calculator.calculate(i.a, i.b, i.operation)

With those few lines of code you get a web application that exposes an endpoint /calculate with the following features: - accepts HTTP POST requests on that endpoint - expects a JSON mimetype request body - will parse that body into object i according to the schema in CalculateIn - will validate and convert the request body and raise an error if it doesn’t adhere to the schema - e.g. operation is a categorical str choice with only the 4 options defined in the Enum as possible values - will return an HTTP response with the function return as a JSON mimetype body - will validate and convert the return value according to the response_model type assignment - oh and it will even automatically create an openAPI schema, which you can e.g. see as swagger at /docs

I usually dislike when libraries are too magical (like e.g. how pytest injects fixtures into tests). But with fastAPI, even though many things are going on behind the curtain, you never feel out of touch. I mainly attribute this to the fact that due to the ubiquitous type information it’s always easy to reason about all the objects you encounter.

For similar functionality I have previously used flask, and of course the way you define endpoints via decorators looks very familiar. But in flask, many of the features described above, which are great when trying to develop a robust JSON API, just arent’t there or feel more like an afterthought. For example: flask, by default, is very focused on returning a plain-text body, not JSON. Also, there is no input/output validation in vanilla flask at all. And any extensions that offer this kind of functionality (e.g. flask-restplus) never really deliver this nice and extremely integrated experience you get with fastAPI.

Standing on the shoulders of giants

Anyways, I’ll stop the love letter to fastAPI here. What I actually want to talk about in this post is the black hole that I got sucked into when I wanted to find out a bit more about what actually powers fastAPI under the hood.

When you read the documentation, you’ll quickly see that fastAPI gives a lot of credit to a couple of libraries that it builds on top of, namely starlette (a web application framework) and pydantic (a data validator based on python typing information).

graph LR fastAPI---starlette fastAPI---pydantic

When you read a bit further, once you have defined an application (as in the example above, say in a file called main.py) it will tell you to now serve the web application by doing something like the following:

uvicorn main:app --reload

So your fastAPI app isn’t actually a webserver itself, and it requires a webserver (in this case uvicorn) to get served and be able to receive HTTP requests and respond to them.

graph LR uvicorn---fastAPI subgraph webserver uvicorn end subgraph web application fastAPI---starlette fastAPI---pydantic end

To me this split was quite interesting, and something I had not thought much about before. And it’s actually the same for flask, even though it’s initially less obvious. When developing with flask the usual way to start serving your application looks something like this:

export FLASK_APP=main.py
flask run

On a first glance, it looks like flask is serving itself. But actually (as is made clear in the documentation) it is itself tightly integrated with a library called werkzeug which in turn contains a webserver implementation. So when you’re running your application via the flask cli, it gets served through the werkzeug webserver.

Actually I like the explicit approach of fastAPI better. You can clearly see the boundaries of responsibility. The fastAPI application is in charge of providing the functionality for when certain paths are called on the webserver. uvicorn is that webserver and handles the raw HTTP communication with the client and calls the application for generating the appropriate responses to incoming requests.

WSGI and ASGI

The most interesting part about this is the communication protocol used between the webserver and the application. For flask it is called WSGI (Python Web Server Gateway Interface) and it’s a protocol standard in the python world that basically allows you to pair up any webserver with any application as long as they speak this protocol. And indeed this is routinely done with flask applications, where it is recommended not to use werkzeug in a production setting and instead swap it out for e.g. uwsgi.

The same holds true for fastAPI. It is an application that adheres to the ASGI protocol (Asynchronous Python Web Server Gateway Interface), which is the successor of WSGI for webservers and applications that want to use coroutine-based concurrency to handle many parallel connections (instead of e.g. threads in WSGI). And again, if webservers and applications speak this protocol, you can pair them up at random. In the fastAPI documentation you can find that you can equally well serve your application with a webserver called gunicorn instead of uvicorn. (I really don’t know where this obsession with unicorns comes from.)

graph LR uvicorn---|ASGI|fastAPI subgraph webserver uvicorn end subgraph web application fastAPI---starlette fastAPI---pydantic end

So the more I dug into the dependencies of fastAPI and how you deploy it, the more I realized how little I actually knew about the ins and outs of using python on the web.

And I believe the best way to learn about a piece of technology is to try and build it yourself from the ground up. So let’s do just that, step-by-step, while simultaneously improving our understanding of: - TCP - HTTP - webservers, web applications and web frameworks in python - WSGI protocol / servers / applications - ASGI protocol / servers / applications