Lunch Time Python¶
Lunch 14: FastAPI¶
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
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 ofusername: ...
- 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¶
from fastapi import FastAPI
app = FastAPI()
# 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¶
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
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
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
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
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¶
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
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
@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¶
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:
Swagger API UI¶
A swagger UI is also generated, which allows you to interact with the API from the browser:
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
- SQLModel+FastAPI tutorial
- Litestar (alternative to FastAPI)
- Hey API getting started
- Example project