Welcome to my field notes!

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.
Author

TJ Palanca

Published

October 15, 2022

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.

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
Order does matter. If you define a path parameter after one the generally matches, it will always match on that one.

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 from fastapi to add additional validation to query parameters.

    @app.get("/items/")
    async def read_items(q: str | None = Query(default=None, max_length=50)):
        results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
        if q:
            results.update({"q": q})
        return results
  • Use ellipsis ... or pydantic.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 parameter

  • Use 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):
        result = {"item_id": item_id, **item.dict()}
        if q:
            result.update({"q": q})
        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(
        *,
        item_id: int,
        item: Item,
        user: User,
        importance: int = Body(gt=0),
        q: str | None = None
    ):
        results = {"item_id": item_id, "item": item, "user": user, "importance": importance}
        if q:
            results.update({"q": q})
        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 and Form fields in the same handler.

Receiving files

  • Use fastapi.File (bytes), or fastapi.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 an Exception to return an error to the client. If you want to return a specific status code, use fastapi.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 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(
            status_code=418,
            content={"message": f"Oops! {exc.name} did something. There goes a rainbow..."},
        )

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 model
    • response_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, use fastapi.status as convenience functions to specify the codes.
  • 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 callable f that is applied to the input parameters - it can be a function or a class. If it’s a class, you can skip the Depends(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 and fastapi.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 with generate_{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

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 by include_router-ing them. This can be prefixed at a certain file so that everything is in that subpath.
  • An APIRouter can also include_router another APIRouter.

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 call add_task on it with the function to run in the background.

Documentation

  • Use schema_extra field in the Config class of a Pydantic models to define example data thatt the API can receive.

    class Item(BaseModel):
        name: str
        description: str | None
        price: float
        tax: float | None
    
        class Config:
            schema_extra = {
                "example": {
                    "name": "Foo",
                    "description": "A very nice Item",
                    "price": 35.4,
                    "tax": 3.2,
                }
            }
  • Path, Query, Header, Cookie, Body, Form, and File 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.

    app.mount("/static", StaticFiles(directory="static"), name="static")

Testing

  • Create a fastapi.testclient.TestClient instance, that you can call just like requests 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 a render() method to output the text response to a content stream.
  • 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.
  • 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

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:
        data = await websocket.receive_text()
        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 an async 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