• SSC Lunch Time Python
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

Lunch Time Python¶

Lunch 14: FastAPI¶

No description has been provided for this image

FastAPI is a web framework that combines the starlette ASGI framework with pydantic data validation. Similarly, SQLModel combines the SQLAlchemy database toolkit with pydantic. Together they allow you to write expressive web backends with data validation using Python type hints.

Press Spacebar to go to the next slide (or ? to see all navigation shortcuts)

Lunch Time Python, Scientific Software Center, Heidelberg University

No description has been provided for this image https://medium.com/@adityagaba1322/streamlining-backend-frontend-integration-a-quick-guide-145eca3cca05

Motivation¶

Sending / recieving data¶

  • A key part of a web development is receiving/validating/sending data, e.g.
    • Frontend sends json data to the backend
    • Backend parses and validates json
    • Backend creates database object from data
    • Backend returns database object as json to frontend

Parsing / validation data¶

  • Need validation logic
    • Check that required fields are present
    • Check that the field has the right type
  • Needs to be done with every send / receive of data
    • Ideally on both sides
  • This involves a lot of boilerplate code
  • Also often involves duplicating types and validation logic between frontend and backend

Making changes¶

What happens when you modify a data structure?

  • You need to update the logic that uses this data structure
  • You need to update all the associated validation logic

Prerequisite

  • You first need to find all the places it is used
  • For a common variable name like a "name" field, even this may not be straightforward!

Writing code¶

What happens when you make a coding mistake or typo, e.g.

  • Use the wrong endpoint: /userss/ (or even /users instead of /users/!)
  • Use the wrong key: email: ... instead of username: ...
  • Use a key that doesn't exist in the data
  • etc

All these are perfectly fine from the IDE / code point of view - so if you're lucky you'll find the error when you run your testsuite, if not only with manual testing or worst case in production.

In a better world...¶

  • data schema would be defined as types in a single place in your code
  • all backend endpoints would automatically validate inputs and outputs according to their types
  • frontend code would have automatically generated type definitions for data and endpoints

Benefits¶

  • many errors are now type errors instead of runtime errors
  • this means your IDE can catch them as you write them
  • your IDE can also be much more helpful with auto-complete / types / API function lookup

FastAPI + SQLModel¶

  • FastAPI
    • starlette (ASGI framework)
    • pydantic (data validation)
  • SQLModel
    • sqlalchemy (database toolkit)
    • pydantic (data validation)

Comparison with Flask¶

Layer Typical Flask stack This talk
Interface WSGI ASGI
Server Gunicorn Uvicorn
Framework flask / werkzeug starlette
Framework data validation flask-pydantic fastAPI / pydantic
Database sqlalchemy sqlalchemy
Database data validation ? SQLModel / pydantic

FastAPI app¶

In [1]:
from fastapi import FastAPI

app = FastAPI()
In [2]:
# database boilerplate
from typing import Annotated
from fastapi import Depends
from sqlmodel import Session, create_engine

engine = create_engine("sqlite:///tmp.db")


def get_session():
    with Session(engine) as session:
        yield session


SessionDep = Annotated[Session, Depends(get_session)]

SQLModel data models¶

In [3]:
from sqlmodel import Field, SQLModel


class User(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    name: str
    password_hash: str


class UserRead(SQLModel):
    id: int
    name: str


class UserCreate(SQLModel):
    name: str
    password: str


class UserUpdate(SQLModel):
    name: str | None = None
    password: str | None = None


SQLModel.metadata.create_all(engine)

FastAPI /users route¶

/users-v1/¶

Returns some data

In [4]:
from sqlmodel import select


@app.get("/users-v1/")
def read_users_v1(session: SessionDep):
    users = session.exec(select(User)).all()
    return users

...but we don't specify what data!

/users-v2/¶

Returns a list of User objects

In [5]:
from sqlmodel import select


@app.get("/users-v2/", response_model=list[User])
def read_users_v2(session: SessionDep):
    users = session.exec(select(User)).all()
    return users

...but we don't want to include the password hash!

/users-v3/¶

Returns a list of UserRead

In [6]:
from sqlmodel import select


@app.get("/users-v3/", response_model=list[UserRead])
def read_users_v3(session: SessionDep):
    users = session.exec(select(User)).all()
    return users

...but what if we have a million users in our database?

/users/¶

Returns a list of UserRead, with an offset and a limit

In [7]:
from fastapi import Query
from sqlmodel import select


@app.get("/users/", response_model=list[UserRead])
def read_users(
    session: SessionDep,
    offset: int = 0,
    limit: int = Query(default=100, le=100),
):
    users = session.exec(select(User).offset(offset).limit(limit)).all()
    return users

More FastAPI routes¶

In [8]:
from fastapi import HTTPException


@app.get("/users/{user_id}", response_model=UserRead)
def read_user(session: SessionDep, user_id: int):
    user = session.get(User, user_id)
    if user is None:
        raise HTTPException(404)
    return user
In [9]:
from argon2 import PasswordHasher


@app.post("/users/", response_model=UserRead)
def create_user(session: SessionDep, user: UserCreate):
    password_hash = PasswordHasher().hash(user.password)
    extra_data = {"password_hash": password_hash}
    user_db = User.model_validate(user, update=extra_data)
    session.add(user_db)
    session.commit()
    session.refresh(user_db)
    return user_db
In [10]:
@app.patch("/users/{user_id}", response_model=UserRead)
def update_user(session: SessionDep, user: UserUpdate, user_id: int):
    user_db = read_user(session, user_id)
    user_data = user.model_dump(exclude_unset=True)
    if user.password:
        password_hash = PasswordHasher().hash(user.password)
        user_data["password_hash"] = password_hash
    user_db.sqlmodel_update(user_data)
    session.add(user_db)
    session.commit()
    session.refresh(user_db)
    return user_db

Run the API¶

In [11]:
import uvicorn

# uncomment to run the app in this cell:
# await uvicorn.Server(uvicorn.Config(app)).serve()

ReDoc API documentation¶

From the openapi schema ReDoc API docs are generated:

http://localhost:8000/redoc

Swagger API UI¶

A swagger UI is also generated, which allows you to interact with the API from the browser:

http://localhost:8000/docs

Typescript types and client¶

There are a variety of tools that can autogenerate both types and clients in typescript for your backend.

For example:

pnpx @hey-api/openapi-ts -i http://localhost:8000/openapi.json -o src/client -c @hey-api/client-fetch

This generates the types for our API:

export type UserRead = {
    id: number;
    name: string;
};

export type ReadUsersUsersGetResponse = (Array<UserRead>);
export type ValidationError = {
    loc: Array<(string | number)>;
    msg: string;
    type: string;
};

export type HTTPValidationError = {
    detail?: Array<ValidationError>;
};

export type ReadUsersUsersGetError = (HTTPValidationError);

As well as a client for interacting with it:

/**
 * Read Users
 */
export const readUsersUsersGet = <ThrowOnError extends boolean = false>(options?: Options<unknown, ThrowOnError>) => {
    return (options?.client ?? client).get<ReadUsersUsersGetResponse, ReadUsersUsersGetError, ThrowOnError>({
        ...options,
        url: '/users/'
    });
};

Summary¶

With FastAPI + SQLModel

  • Data schema is defined in a single place for the backend / frontend / database
  • Automatic validation of data in backend and frontend
  • Auto-generated API docs / UI
  • Auto-generated API client typescript code

More information¶

  • FastAPI tutorial
    • https://fastapi.tiangolo.com/tutorial/
  • SQLModel+FastAPI tutorial
    • https://sqlmodel.tiangolo.com/tutorial/fastapi/
  • Litestar (alternative to FastAPI)
    • https://litestar.dev/
  • Hey API getting started
    • https://heyapi.dev/openapi-ts/get-started.html
  • Example project
    • https://github.com/ssciwr/mondey