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).
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.
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.)
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