FastAPI for Django Programmers: Dependency Injections, Middleware, and Error Handling


FastAPI for Django Programmers: Dependency Injections, Middleware, and Error Handling

In my previous posts, I went into adding features to FastAPI to make it more Django like, but in this article, I’m going to go into, what I think, is the best features of FastAPI, and how these can be used to adding many of the features you know and love, from Django.

Dependency Injection

This is one of those annoying topics, where everyone says how great it is, but explanations fall short. In a nutshell, a dependency injection is a function or class, that is run before your view is run. Maybe a better description is a view that is run before your view.

In this, I mean the dependency gets passed and has access to all the arguments that would be passed to your view by default, thus its treated like a view. However its not expected, and indeed, cannot return a response object.

Lets say, you have five query views, all dealing with a “person” object. You can write a dependency function that will grab the “person_id” query parameter and get the record from the database. it them returns that object, so your view gets passed the “person” object, instead of you having to get the record in each view.

from fastapi import Depends, FastAPI, HTTPException
from models import Person


app = FastAPI()

# Dependency function
async def get_record(person_id: int)):
record = await Person.get(person_id)
if record is None:
raise HTTPException(status_code=404, detail="Item not found")
return record

# FastAPI function
@app.get("/items/{person_id}")
async def read_item(person: Person = Depends(get_record)):
return item

So your thinking to yourself that this is ok for following DRY, but what’s so great about it. You need to think about this a little and you can see where dependency injections can be used in 100’s of places.

One example would be having a dependency that pulls all your form data and validates it before the view starts. The data could also be reformatted where needed. Thus, this allow you to separate data validation and preparation, from processing. Secondly, just as with validation, you can use this to run one or more tests, such as session and user authentication before the view starts.

Now, add this with the fact that you can have multiple dependencies for each view. Now add that your dependencies, can themselves have dependencies.

Dependency Injection now looks like a powerful feature, and one that can easily be used to write all sorts of nice functionality into your system. Hang on a moment though because there are a couple of things you need to know.

First, look back on the dependency above and you will see it raises an exception if the record is not found. A limitation on a dependency is that you cannot return a “response” object, so you cannot, say redirect to another url, if there is a problem, only raise an error.

FastAPI already has a solution to this. Error Handling.

So whats that? Well in FastAPI you can code a view that will be run upon a set exception being raised.

from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse

class PersonException(Exception):
def __init__(self, message: str):
self.detail = message

app = FastAPI()

@app.exception_handler(PersonException)
async def custom_exception_handler(request: Request, exc: PersonException):
return JSONResponse(
status_code=400,
content={"message": f"Oops! {exc.name} did something. Here's the detail: {exc.detail}"},
)

# Dependency function
async def get_record(person_id: int)):
record = await Person.get(person_id)
if record is None:
raise PersonException
return record

# FastAPI function
@app.get("/items/{person_id}")
async def read_item(person: Person = Depends(get_record)):
return item

From the code above, I have created a custom exception, PersonException, which is raised if the Person object is not found. If this is raised, then the exception_handler is activated. This can then send the Response you want, including redirecting.

I very much like Exception Handling. It just seems like such a neat solution.

Defining Dependencies in other places.

Dependencies are not limited to being defined in the view parameters, but can be defined in the path decorator, APIRouter object and FastAPI object. There is however one difference you need to be aware of. Only dependencies defined in the view, can return results. Dependencies defined in other places are pass/fail. They either return nothing, or raise an exception.

from fastapi import Depends, FastAPI, HTTPException
from pydantic import BaseModel

# Hypothetical "Database" class for db operations (replace with real implementation)
class Database:
async def get_item(self, item_id: int):
# Replace with actual implementation
return {"example_item"}

async def close_connection(self):
# Replace with actual implementation
pass

app = FastAPI()

# Dependency function
async def get_record(db: Database = Depends(Database)):
record = await db.get_item()
if record is None:
raise HTTPException(status_code=404, detail="Item not found")
return record

# FastAPI function
@app.get("/items/", dependencies=[Depends(get_record)])
async def read_items():
# Assuming that get_record returns some item record
items = await get_record()
return items

Above is an example of a dependency defined in the path decorator. Notice that the dependencies parameter requires a list, thus you can pass multiple dependencies. However, there is no way to get data out of these dependencies.

Middleware

Middleware is something, most of us define in the settings and have very little to do with other than that. They are just functions that can modify the request object before its passed to the view and then modify the response object once the view has completed.

With FastAPI, I have found that Dependency Injection deals with the majority of such tasks, but it does have an easy way to define middleware that is very easy to understand.

from fastapi import FastAPI

app = FastAPI()

@app.middleware("http")
async def add_custom_header(request, call_next):
response = await call_next(request)
response.headers["X-Custom-Header"] = "This is a custom header"
return response

@app.get("/")
def read_root():
return {"message": "Hello, World!"}

So from this, you can see that two arguments are passed, the request and something called “call_next”. “call_next” is actually your view function. in this example, the request is not modified, the view run, and a response returned. Then a header value is added to the response.

This is a very simple system, and easy to use, but its global, and doesn’t have the granular capabilities that dependency injections do.

Conclusion

These features are ones, that can be used to take your projects to mega high levels, allowing you to expand your base code, and add custom functionality all over the place. This makes turning FastAPI into whatever type of base system you like, very easy.