updated April 25, 2023

What's FastAPI?

Fast API is a modern Python web framework that provides cutting edge features:
  • type hints for validation and parsing request URL parameters
  • pydantic models to parse and validate JSON body data
  • auto-generated project API documentation via Swagger
  • async request handlers
  • code re-use via dependency injection

Lets find out how to create a simple one file FastAPI application and respond to HTTP GET and POST requests.

1. The simplest application.

Create project folder, change into it. Install dependencies in virtual env:
mkdir fastapi-demo
cd fastapi-demo
python3 -m venv env
source env/bin/activate
python3 -m pip install fastapi uvicorn
Create main.py application file in the fastapi-demo folder:
from fastapi import FastAPI

app = FastAPI()  # FastAPI application instance


@app.get("/")
def root():
  """
  Path function or handler for HTTP GET / request. 
  """
  return "FastAPI demo"

When the GET request to / URL is made, the FastAPI application calls the root() function, because the value of app.get() parameter matches the request URL. Such function for handling HTTP requests is called a path function.

Run in command line in the fastapi-demo folder to start web server:
uvicorn --reload main:app
The FastAPI application app instance from the main.py is listening HTTP requests on http://127.0.0.1:8000 so lets open the URL in a browser.end

Or make the HTTP request from command line:

curl http://127.0.0.1:8000

And lets try it from the Swagger project API documentation, one of FastAPI key features: open http://localhost:8000/docs/ in a browser, expand the GET / root section and press the Try it out button to trigger the HTTP request.

2. Handling HTTP GET request parameters.

There are two ways to add parameters in the URL:

Path

To handle URL like /users/user_id/ where user_id is varied integer value, for example /users/12/ if user_id is 12, we should add the parameter name in curly brackets into the app.get() url parameter and add the parameter with the same name into the path function:
@app.get("/users/{user_id}/")
def user_details(user_id: int):
  return {'user_id': user_id}
The optional type hint is used for the parameter type validation and parsing. Command line request:
curl http://localhost:8000/users/12/
Swagger: open http://localhost:8000/docs, expand the User Details section, fill the integer user_id field and press the Try it out button.

Query string

To handle a parameter passed after ? character in the request URL, we add only the path function parameter with the same name as the parameter in the query string, for example the path function below gets the q query string parameter:
@app.get("/search/")
def search(q: str):
  return {'q': q}
The parameter type is restricted to a primitive types like integer, string or boolean. Command line request:
curl http://localhost:8000/search/?q=abc567
Also the parameter is required so making the http://localhost:8000/search/ request without the parameter returns the missing parameter error. To fix this, we'll make the parameter optional, depending on the Python version:
q: Optional[str]   # before Python 3.10
q: str | None = None  # Python 3.10 or later
The first option requires Optional type import from typing module.

Both of them: URL contains URL path and query string parameters

In this case the parameters from the URL path are matched first, then other parameters are considered query string parameters:
@app.get("/users/{user_id}/orders/")
def user_orders(user_id: int, q: str):
  return {'user_id': user_id, 'q': q}
Command line request:
curl http://localhost:8000/users/12/orders/?q=abc567

3. The path functions order matters.

When the application file contain several path functions, the first path function with the URL match is called. Let's figure out which function is called for /users/about/ URL in the application below:
@app.get("/users/{username}/")
def user_details(username: str):
  return "user detail"


@app.get("/users/about/"):
def users_about():
  return "Users list"
The user_details() is called because it's the first and /users/{username}/ matches the URL as well. So we should place the users_about() path function first in the file to be called on the /users/about/ URL:
@app.get("/users/about/"):
def users_about():
  return "Users list"


@app.get("/users/{username}/")
def user_details(username: str):
  return "user detail"

4. Handling HTML form POST data.

FastAPI provides POST request support via @app.post(url) decorator. Let's explore how to access the POST body parameters in case of HTML form is posted in a browser. First install the package for FastAPI form data support:
python3 -m pip install python-multipart
and restart the application. Then add to main.py the code below:
from fastapi import Form

@app.post("/login/")
def login(username: str = Form(), password: str = Form()):
  return {"logged": username}
A parameter with Form() default value is processed as multipart form data POST parameter instead query parameter.

Let's test the login endpoint via Swagger or command line:

curl -F username=alex -F password=123 http://localhost:8000/login/

Expected output: {"logged":"alex"}

Handling POST data in JSON format.

Let's assume JSON is sent from a browser by JavaScript, in the POST request body. The code below adds endpoint that accepts JSON in format {"username": str, "password": str}:
from pydantic import BaseModel


class NewUser(BaseModel):
  username: str
  password: str


@app.post("/register/")
def register(new_user: NewUser):
  return {"registered": new_user.username}

We added a parameter of NewUser class to a path function so the application gets it from the POST request body instead query string. Sub-class of pydantic.BaseModel describes JSON format to convert the JSON string to an object.

Command line request:
curl -X 'POST' http://localhost:8000/register/ -H 'accept: application/json' -H 'Content-Type: application/json' -d '{"username": "alex", "password": "na12"}'
JSON passed in the POST request body could contain a list or JSON field, not only primitive types, example:
{
  "username": "alex",
  "tags": [
    "user", "test"
  ],
  "location": {
    "country": "Ukraine",
    "city": "Kharkov",
    "address": "some street"
  }
}
the corresponding validation and parsing model:
from typing import Optional, List


class Location(BaseModel):
  country: str
  city: str
  address: str
    
    
class NewUser(BaseModel):
  username: str
  tags: Optional[List[str]]
  location: Location