DRY Route Handlers with ProjectDep
With SessionDep defined in dependencies.py, every route that touches the database has a clean way to ask for a session. That handles half of the boilerplate. The other half is the lookup-or-404 dance that shows up on most routes.
The repeating 404 pattern
The endpoints for fetching, updating, and deleting projects all take a project_id, query the database, and raise a 404 if the project doesn't exist:
project = crud.get_project(session, project_id)
if not project:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Project not found"
)
Three routes, three copies of the same five lines. As more routes get added (and as a Task model joins the API), that boilerplate compounds.
We can collapse it by writing one dependency that handles the lookup and the 404, then using it everywhere a project is needed. Call it get_project_or_404.
from typing import Annotated
from fastapi import Depends, HTTPException, status
from sqlmodel import Session
from . import crud
from .database import get_session
from .models import Project
SessionDep = Annotated[Session, Depends(get_session)]
def get_project_or_404(project_id: int, session: SessionDep) -> Project:
project = crud.get_project(session, project_id)
if project is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Project not found"
)
return project
ProjectDep = Annotated[Project, Depends(get_project_or_404)]
Typing a route handler argument as ProjectDep removes the 404 boilerplate from the route entirely. The route only deals with the happy path:
@app.get("/projects/{project_id}", response_model=ProjectRead)
def get_project(project: ProjectDep):
return project
If the project doesn't exist, the request never reaches the route. FastAPI raises the 404 from inside get_project_or_404 and returns the response.
Future Refactoring
For now, get_project_or_404 is hardcoded for the Project model. We'll add get_task_or_404 when we add the Task model.
For larger APIs there are patterns to avoid one dependency per model, but they're out of scope for this course.