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
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:
POST requests on that endpoint
- expects a
JSON mimetype request body
- will parse that body into object
i according to the schema in
- will validate and convert the request body and raise an error if it doesn’t adhere to the schema
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
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
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
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
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
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.
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:
- webservers, web applications and web frameworks in python
WSGI protocol / servers / applications
ASGI protocol / servers / applications