FastAPI Mental Model

20 March 2024

Goal

I started writing this to clarify for myself how FastAPI works. It starts from a high-level and doesn't go very in depth. The reason I stopped is that I realised I'm not a fan of FastAPI's design choices. I realised I prefer the API design of Django Rest Framework and its little sister Django Ninja.

I decided to publish this anyway. I know it's pretty light, but maybe it helps create a clearer image in someones mind.

Start

FastAPI is a Web Framework.

Client <-> Server

The Web works with a Client - Server Architecture.

An example of a Client is a Browser or a Library like Python's requests.

An example of a Server is nginx or Apache.

Request <-> Response

The Client sends a Request to the Server. The Server processes the Request and returns a Response.

API

FastAPI, just like the name says, allows you to build APIs (REST or GraphQL).

An API is a collection of Endpoints, with each Endpoint corresponding to a Resource.

An Example of a Resource is Author or BlogPost or Comment or anything else you can think of.

The REST convention is to use the plural in the resource Path, so the corresponding Paths are /authors, /blogposts, /comments, /anythings. I don't know about GraphQL, because I haven't used it.

Also, a specific Endpoint provides the 4 Operations that allow you to manage that Resource:

Endpoint

The most common way of using FastAPI is by defining Endpoints.

A FastAPI Endpoint consists of 2 parts:

Decorator

The Decorator specifies things like

Function

The Function specifies things like

So the function doesn't take the entire Request as an argument. It only specifies which parts of the request it needs. Then, it processes those, does its thing, and returns a Value that is further used to make up the Response. Notice, I didn't say it "returns the Response", because normally FastAPI is responsible for creating the Response from the Function's return value.

Path Parameters

async def read_item(item_id: int):

Query Parameters

You specify them in 2 ways:

async def read_item(skip: int = 0, limit: int = 10):

OR

async def read_items(skip: Annotated[int | None, Query()] = 0):

Path Params vs Query Params

I assume FastAPI distinguishes between Path Parameters and Query Parameters by looking at the Path you defined. It matches any Parameters you defined there with Arguments you defined for the Function.

To speak in code, if you have this Endpoint:

@app.get("/items/{item_id}")
async def get_item(item_id: int):
    pass

FastAPI knows that item_id is coming from the Path.

However, if you make a mistake and you define this Endpoint:

@app.get("/items/")
async def get_item(item_id: int):
    pass

FastAPI will look for item_id among the Query Parameters.

Headers

async def read_items(user_agent: Annotated[str | None, Header()] = None):

Cookies

async def read_items(ads_id: Annotated[str | None, Cookie()] = None):

Capabilities

By Capabilities I mean Functionality that you usually need in an API:

Aside about CORS

CORS (Cross-Origin Resource Sharing)

It exists as an Exception to Same-Origin Policy. The Same-Origin Policy allows a Browser App (like a Single-Page Application) or a simple Script to send Requests only Home. Home means the same Server where it came from. I guess Origin is the better word. So, normally the Script can only send Requests to its Origin Host.

CORS allows the Script to send Requests to other Hosts. It involves cooperation from the specific Host, because the Host must say in its Response that it allows Requests from the Script Origin. When it sees that, the Browser allows the Requests, so the Browser is the one that enforces this.

Middleware

Capabilities can be implemented as Middleware.

Middleware can be visualised as a circle that surrounds an Endpoint. It processes a Request before it's passed to the Endpoint and it again processes the Response returned by the Endpoint.