Field notes are notes I leave myself as I go through my day to day work. The hope is that other people will also find these notes useful. Note that these notes are unfiltered and unverified.
FastAPI
FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints. It puts Python at par with modern frameworks like NodeJS, so this allows me to keep using Python for my backend.
Essentially, building an app involves build a set of functions that return an output, and adding decorators around those functions in order to define the input and output types.
Key Links
Request Parameters
- Parameters are the inputs to the function, as variables. They are the inputs that are sent by whoever is requesting information.
Path Parameters
Path parameters are embedded in the path:
@app.get("/items/{item_id}") async def read_item(item_id): return {"item_id": item_id}
Type hint the params to convert them from their string representation
If you want to constrain the values, use an
Enum
and inherit from the base type that it needs to be converted to:class ModelName(str, Enum): = "alexnet" alexnet = "resnet" resnet = "lenet" lenet @app.get("/models/{model_name}") async def get_model(model_name: ModelName): if model_name is ModelName.alexnet: return {"model_name": model_name, "message": "Deep Learning FTW!"}
Import
fastapi.Path
to declare additional metadata.- You can also use this to get around the fact that args with default values should be last in a function signature.
- You can add validations:
gt
,le
Query Parameters
Function arguments that are not part of the path are query parameters.
You can make them optional by specifying
None
as a default value. This one will respond to/items/1/?q=2
:@app.get("/items/{item_id}") async def read_item(item_id: str, q: str | None = None): if q: return {"item_id": item_id, "q": q} return {"item_id": item_id}
As usual, use type annotations to convert the query parameters to the correct type.
Use the
Query
object fromfastapi
to add additional validation to query parameters.@app.get("/items/") async def read_items(q: str | None = Query(default=None, max_length=50)): = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} results if q: "q": q}) results.update({return results
Use ellipsis
...
orpydantic.Required
as default value to declare it as a required variable.You can receive a list of things in query if it is specified more than once, like so:
/items/?q=foo&q=bar
.Use
alias
to specify an alternative name for the query parameterUse
Query(include_in_schema=False)
to exclude from the documentation
Body Parameters
Supply
{pydantic}
models in order to produce more complex types. Complex types will need to be sent via the request body.@app.put("/items/{item_id}") async def create_item(item_id: int, item: Item, q: str | None = None): = {"item_id": item_id, **item.dict()} result if q: "q": q}) result.update({return result
The resolution is as follows:
- Declared in path? It’s a Path Parameter
- Singular type (
int
,float
,str
,bool
, etc.)? It’s a query parameter - Pydantic model? It’s a body parameter.
If there are more than one Pydantic models in the arguments, then it will need the overall JSON body to be composed of those two objects identified by the key.
You can use
fastapi.Body
to specify singular objects as body parameters in addition to just the Pydantic models.@app.put("/items/{item_id}") async def update_item( *, int, item_id: item: Item, user: User,int = Body(gt=0), importance: str | None = None q: ):= {"item_id": item_id, "item": item, "user": user, "importance": importance} results if q: "q": q}) results.update({return results
Header Parameters
Header()
can extract data from the HTTP header.- It automatically converts snake case to the kebab title case common in headers.
- In case of duplicate headers, then a list is resolved.
Using {pydantic}
models
- Use
pydantic.Field
to add addtiional validation to fields within objects that you use as parameters. - Nested models can be done, such as lists, sets, tuples, or dicts.
- Other useful data types:
UUID
,datetime.datetime
,datetime.date
,datetime.time
,datetime.timedelta
,frozenset
,bytes
,Decimal
and others defined by Pydantic.
Receiving form data
If we are not received we may be receiving form data.
@app.post("/login/") async def login(username: str = Form(), password: str = Form()): return {"username": username}
You can’t mix
Body
andForm
fields in the same handler.
Receiving files
Use
fastapi.File
(bytes), orfastapi.UploadFile
(streaming) to receive files.@app.post("/files/") async def create_file(file: bytes = File(...)): return {"file_size": len(file)}
Errors
At any point, you can
raise
anException
to return an error to the client. If you want to return a specific status code, usefastapi.HTTPException
.@app.get("/items/{item_id}") async def read_item(item_id: str): if item_id not in items: raise HTTPException(status_code=404, detail="Item not found") return {"item": items[item_id]}
- You can add custom headers to this
HTTPException
to supply more metadata.
- You can add custom headers to this
You can override the request validation error.
Exception Handlers
Exceptions are by default just raised and sent back to the client, but we can handle exceptions differently by registering exception handlers.
class UnicornException(Exception): def __init__(self, name: str): self.name = name @app.exception_handler(UnicornException) async def unicorn_exception_handler(request: Request, exc: UnicornException): return JSONResponse( =418, status_code={"message": f"Oops! {exc.name} did something. There goes a rainbow..."}, content )
Response
- This is what is returned by the API. You need
{python-multipart}
Pydantic for the response
- Specify in the decorator’s
response_model
argument the Pydantic model to which the return value will be coerced. - You can use inheritance to differential the request and response model for CRUD applications.
- request model that is from the client
- output model that should be quite secure
- database model that needs to apply encryption at rest.
- Modify the response model in the decorator:
response_model_exclude_unset=True
means that default values are not included in the returned modelresponse_model_include={"name", "description"}
means that only the specified fields are included in the returned model.response_model_exclude={"tax"}
means that the specified fields are excluded from the returned model.
Decorator
- Status code can be set:
- in the
status_code
argument of the decorator, usefastapi.status
as convenience functions to specify the codes.
- in the
- Tags can be set for the documentation. Use an enum to constrain tags.
- Summary and description for documentation.
- Response description for documentation in case docstring is inappropriately or too verbose.
Dependencies
- Dependency injection means that the code declares some things that it needs to work and use (“inject” those dependencies).
fastapi.Depends(f)
can be used to declare a dependency callablef
that is applied to the input parameters - it can be a function or a class. If it’s a class, you can skip theDepends(f)
argument if you already have the class instance in the type annotation.- This is integrated with OpenAPI for that documentation.
- Dependencies can use more dependencies, and they are cached. Use
use_cache=False
to disable caching. - You can declare
dependencies
argument in decorator to declare dependencies that don’t get used (like verification). - Global dependencies can be declared in
fastapi.APIRouter
andfastapi.FastAPI
instances to apply across a group of handlers. - Using
yield
in a dependency will allow you to run code after the handler is done. Sub dependencies can be generated withgenerate_{name}()
.- You should encapsulate yield in try blocks because exceptions won’t be handled anymore.
Security
fastapi.security
contains these utilities. It’s essentially a set of middleware that applies some common security policies.
Middleware
- Middleware is a function that works with every request before it is processed by the path operation.
- You can mutate the request and response after the path operations have run.
- It takes the request, call_next handlers, then returns the response.
- Integrated middleware
CORSMiddleware
is a middleware that takes some allowed origins, credentials, methods, and ehaders then only allows those to go through.HTTPSRedirectMiddleware
is a middleware that redirects all HTTP requests to HTTPS.TrustedHostMiddleware
is a middleware that only allows requests from trusted hosts.GZipMiddleware
is a middleware that compresses responses with gzip.
SQL Databases
- FastAPI is not tied to an ORM, we can use
{SQLAlchemy}
as a standard way this is done. - The creator has created
{SQLModel}
to be more compatible with{FastAPI}
and{pydantic}
. - TODO: We’ll learn this more later as we need it
Larger Applications
- Create a
fastapi.APIRouter
instance for each group of handlers, and put the different router. - Unify them in
main.py
in the root module byinclude_router
-ing them. This can be prefixed at a certain file so that everything is in that subpath. - An
APIRouter
can alsoinclude_router
anotherAPIRouter
.
Background Tasks
- Run after returning a response, like queueing emails or processing data.
- Include an argument of class
fastapi.BackgroundTasks
to the handler, and then calladd_task
on it with the function to run in the background.
Documentation
Use
schema_extra
field in theConfig
class of a Pydantic models to define example data thatt the API can receive.class Item(BaseModel): str name: str | None description: float price: float | None tax: class Config: = { schema_extra "example": { "name": "Foo", "description": "A very nice Item", "price": 35.4, "tax": 3.2, } }
Path
,Query
,Header
,Cookie
,Body
,Form
, andFile
have examples that you can add.You can add metadata to the arguments of
FastAPI
.
Static Files
Mount a static files instance on a path to serve static files. This will not be included in the OpenAPI docs because it is completely independent.
"/static", StaticFiles(directory="static"), name="static") app.mount(
Testing
- Create a
fastapi.testclient.TestClient
instance, that you can call just likerequests
client to test
Advanced
- Return a response directly in order to:
- return a custom status code
- to dictate how to deserialize the response
- the
Response
object has attriubtes that can be set to modify the response.
- Other Responses
fastapi.responses.HTMLResponse
can be used to return HTML responses.fastapi.responses.RedirectResponse
can be used to redirect to another URL.fastapi.response.StreamingResponse
can be used to stream a response.fastapi.responses.FileResponse
can be used to return a file.
- Custom Response classes
- Inherit from
fastapi.Response
, define the fields, and create arender()
method to output the text response to acontent
stream.
- Inherit from
- Change the default response class in the instantiation of
FastAPI
. - Mounting sub applications.
- Two distinct fastapi applications coexisting. No shared code as opposed to adding an
APIRouter
.
- Two distinct fastapi applications coexisting. No shared code as opposed to adding an
- Event handlers
@app.on_event()
can define a function that is run on startup, shutdown
Response
- Take the
response: Response
as an argument and:- use
set_cookie()
to set cookies. - edit the
headers
field dict to edit the headers - edit the
status_code
field to edit the status code
- use
Websocket
- Define the websocket endpoint at some path, and then loop over as we continue to receive websocket messages.
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
while True:
= await websocket.receive_text()
data await websocket.send_text(f"Message text was: {data}")
Pydantic for configuration management
- Inherit from
BaseSettings
to create a class that can be used to manage configuration.- You can then use this throughout the application.
Concurrency
async def
is a coroutine that can be awaited.- If it does not support
async await
then you cannot define anasync
path operation. - This is very useful for longer running operations
Deployment
- Pin your versions as breaking changes are common.
- Deploying in Containers
- Install the dependencies
- Run as normal, with host 0.0.0.0 and port