https://medium.com/@marcnealer/fastapi-after-the-getting-started-867ecaa99de9

FastAPI: After the Getting Started | by Marc Nealer | Medium

Marc Nealer Marc Nealer

If you’ve read any of my other articles, then you will know that I’m a big fan of FastAPI. I used Django for years and other frameworks didn’t really do much to knock if off the number one list, but FastAPI is something special.

In using FastAPI, there are a few things you need to take more notice of, in-order to push your projects forward.

Lifespan Events

FastAPI has the ability to attach tasks that will be run when the app is started and a second set to run when the app stops. Bit like middleware, but wrapping the whole app, not requests.

This was originally controlled by the .on_event() argument, where you could set startup or shutdown.

from fastapi import FastAPI

app = FastAPI()

@app.on_event("startup)
def startup_event():
    with open("log.txt", mode="a" as log:
        log.write("Application Started")

@app.on_event("shutdown")
def shutdown_event():
    with open("log.txt", mode="a") as log:
        log.write("Application shutdown")

This system is being depreciated though. Its still there and you can use it, but they have added a newer version and YOU CAN’T USE BOTH.

The new system allows you to write a context manager and pass this to the FastAPI() object as an argument. If you don’t know, a Context Manager allows code to be run before a block of code and after, such as opening and closing a database connection.

from fastapi import FastAPI, Request
import settings
from contextlib import asynccontextmanager
from typing import AsyncGenerator
from tortoise.contrib.fastapi import RegisterTortoise

@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
    # app startup
    async with RegisterTortoise(
        app,
        db_url=settings.db_url,
        modules=settings.db_modules,
        generate_schemas=True,
        add_exception_handlers=True,
    ):
        # db connected
        yield
        # app teardown


main_app = FastAPI(lifespan=lifespan)

So using this, your startup tasks are added before the yield and shutdown, after the yield. Its quite a neat little idea. Here I’m registering the tortoise ORM at the start of the application. Removal is automatic, so no shutdown action is required.

This is an important stage to understand since FastAPI doesn’t come with any database ORM, or connections. The lifespan is where you need to put these. Also since the function exists for the lifespan on the application, you could start asyncio tasks, which will run the background.

Remember, you can’t use both systems. If you set the lifespan parameter, all on_event() requests will be ignored. Its also worth remembering that on_event() is depreciated, so if your currently using it, you might want to do a quick recode.

Settings

Not really part of FastAPI, but it really needs to be there for all web applications. We need to have a place where the application settings are kept and you can’t put them in your main app file. Placing your settings in your app file results in circular import issues. You need to put them in a different file. I would also advise using python dotenv, so your secret stuff is in env files and not saved to your repo

from dotenv import find_dotenv, dotenv_values
import pathlib
import string

file_path = pathlib.Path().cwd()
static_dir = str(pathlib.Path(pathlib.Path().cwd(), "static"))
config = dotenv_values(find_dotenv(".test_fastapi_config.env"))

db_user = config.get("DB_USER")
db_password = config.get("DB_PASSWORD")
db_host = config.get("DB_HOST")
db_name = config.get("DB_NAME")
db_port = config.get("DB_PORT")
db_url = f"asyncpg://{db_user}:{db_password}@{db_host}:{db_port}/{db_name}"
db_modules = {"models": ["models"]}

max_age = 3600

Middleware

If your coming from a full stack framework like Django, then middlware is something someone else writes. Not so with FastAPI. There are a couple of Middleware modules available, but not much. They document also mentions the middleware functions from Starlette. I tried them and they didn’t work!! Creating middleware is really easy though. FastAPI really makes it so.

Here is a simple, but useful example. I decided to mimic Django’s session system, where the session data is stored in the database and not in the cookie, so the cookie just has a reference to the db record only

from fastapi, import FastApi, Request

main_app = FastAPI(lifespan=lifespan)

@main_app.middleware("http")
async def session_middleware(request: Request, call_next):
    cookie_val = request.cookies.get("session")
    if cookie_val:
        request.scope['session'] = cookie_val
    else:
        request.scope['session'] = "".join(random.choices(settings.session_choices, k=128))
    response = await call_next(request)
    response.set_cookie("session", value=request.session,
                        max_age=settings.max_age, httponly=True)
    return response

As you can see the middleware function gets the request and call_next. the call_next is the actual view function, so the code block before “await call_next(request) is run before the view. The response object is returned from this, so you can then make changes before its finally sent to the user.

All I’m doing here is getting or creating the session key before the view and then making sure its in the cookie when the response is sent.

Exception Handling

This is a brilliant feature, and one to take notice of. You can add listeners for Python exceptions and run a view to return a response to the user. This lets you handle exceptions without sending a http 500 status code.

from fastapi import FastAPI, Request

app = FastAPI()

class PermissionFailedException(Exception):
    def __init__(self, permissions: list):
        self.permissions = permissions


def permission_failed_handler(request: Request, exc: PermissionFailedException):
    """shows an error page if the users authentication scope fails to meet the requirements"""
    return HTMLResponse(content="templates/permission_failed.html", status_code=401)



app.add_exception_handler(PermissionFailedException, permission_failed_handler)

While this is great for catching the unexpected, it works best when paired with Dependency Injections. If you have a Dependency defined to run globally, on the router, or on the decorator, it cannot return any values. It can only finish ok, or raise an exception. To deal with this, create custom exceptions and then write an exception handler to deal with redirects etc.

Dependency Injections

Ok, the documentation goes into these in a lot of detail, but at times it gets a little confusing. One area is the one I’ve just mentioned. Dependencies can be set in a lot of different places. If you place the Dependency in the view path then its expected to return some values as a result.

Anywhere else, it either works or fails. My first thoughts on this was that was a little useless, but when you pair this with the exception handlers, you begin to see a lot if uses for them.

Its best to think of it as having two different Dependency Injection systems. One works on preparing data, and the other validates something before the view runs.

Background Tasks

If you have longer running tasks that your sending to run in a background task manager, then you might be able to this instead, but you need to understand whats going on.

In most systems, a request task is completed when a response object is returned to the user i.e, request comes in, start a task, process, return response, finished. FastAPI treats tasks in a slightly different way because of its async nature. The “return” statement calls a response object, that formats the response and sends it to the user, but fastAPI then looks to see if there are any async tasks started and not yet completed. It doesn’t end the routine until those tasks have completed as well. This means the tasks can still be running AFTER the user has gotten a response.

from fastapi import BackgroundTasks, FastAPI
from asyncio import sleep

app = FastAPI()

def write_notification(email: str, message=""):
    await sleep(1)
    with open("log.txt", mode="w") as email_file:
        content = f"notification for {email}: {message} sent after the user gets a response"
        email_file.write(content)
        
@app.post("/send-notification/{email}")
async def send_notification(email: str, background_tasks: BackgroundTasks):
    background_tasks.add_task(write_notification, email, message="some notification")
    return {"message": "Response sent before the notification is completed"}

A silly example, but it shows what happens. “background_tasks.add_task()” starts an asyncio task, and then the return statement is run. However return calls the default response object, which sends the response to the user and then waits for the background task to complete, before the coroutine completes. Tasks like sending emails, writing out files, to disk, or doing a lot of DB updates can be pushed into these tasks and the response sent to the user, before they have completed.

just remember that your response is already sent, so you can send a response if the task fails.

There is an option in the base response class for setting a background task, but it acts a little differently. With “background_tasks”, the user response and middleware are run and the response sent. With the Request object, middleware won’t be run until the task is completed. Thus if your using middleware to alter responses, then this will interfere with your task.

Learn Pydantic

I can’t stress this one enough. You can skip by with a basic understanding of Pydantic, but its really not enough. Study the module as its fantastic and can bring so much more to your application.

FastAPI is not Starlette

In many areas, we pull objects from Starlette to use. In fact many items such as the Request class are just links to the Starlette class. With that said, don’t be fooled into thinking all the functions in Starlette will work in FastAPI. They won’t.

Go Beyond API’s

FastAPI can handle html responses as well as API’s. There are modules, forwarded from Starlette on handling jinja2, but these are think wrappers. Its easy to add in any templating engine you want.

Final Thoughts

What makes FastAPI special is that it contains all the elements for you to create a fantastic application, but unlike Django, its doesn’t come with preset ways of doing things. Also adding the Batteries that Django has, or some of them is really quite easy.