Home > Software design >  Repeated checking of ID path parameter in FastAPI
Repeated checking of ID path parameter in FastAPI

Time:06-26

I have the following route specs:

GET    /councils/{id}
PUT    /councils/{id}
DELETE /councils/{id}

In all three routes, I have to check in the database whether the council with the id exists, like this:

council = crud.council.get_by_id(db=db, id=id)
    if not council:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail='Council not found'
        )

Which adds to a lot of boilerplate in the code. Is there any way of reducing this? I have thought of creating a dependency but then I have to write different dependency function for different models in my database. Is there any standard practice for this? Thanks.

CodePudding user response:

Here is one solution that works. FastAPI supports classes as dependencies. Therefore I can have a class like this:

class DbObjIdValidator:
    def __init__(self, name: str):
        if name not in dir(crud):
            raise ValueError('Invalid model name specified')
        
        self.crud = getattr(crud, name)

    def __call__(self, db: Session = Depends(get_db), *, id: Any):
        db_obj =  self.crud.get_by_id(db=db, id=id)
        if not db_obj:
            raise HTTPException(
                status_code=status.HTTP_404_NOT_FOUND,
                detail='not found'
            )
        
        return db_obj

Considering crud module imports the crud classes for all the ORM models. My example is closely following the fastAPI cookiecutter project by Tiangolo, and the crud design pattern he followed.

Now in order to use it, for example in councils_route.py, I can do the following:

router = APIRouter(prefix='/councils')

council_id_dep = DbObjIdValidator('council') # name must be same as import name

@router.get('/{id}', response_model=schemas.CouncilDB)
def get_council_by_id(
    id: UUID, 
    db: Session = Depends(get_db),
    council: Councils = Depends(council_id_dep)    
):

    return council

CodePudding user response:

Using a dependency is the way to go - it allows you to extract the logic around "get a valid council from the URL"; you can generalize it further if you want to map /<model>/<id> to always retrieving something from a specific model; however - you might want to validate this against a set of possible values to avoid people trying to make you load random Python identifiers in your models class.

from fastapi import Path

def get_council_from_path(council_id: int = Path(...),
                          db: Session = Depends(get_db),
):
  council = crud.council.get_by_id(db=db, id=id)

  if not council:
    raise HTTPException(
      status_code=status.HTTP_404_NOT_FOUND,
      detail='Council not found'
    )

  return council


@app.get('/councils/{council_id}')
def get_council(council: Council = Depends(get_council_from_path)):
  pass

You can generalize the dependency definition to make it reusable (I don't have anything available to test this right now, but the idea should work):

def get_model_from_path(model, db: Session = Depends(get_db)):
  def get_model_from_id(id: int = Path(...)):
    obj = model.get_by_id(db=db, id=id)

    if not council:
      raise HTTPException(
        status_code=status.HTTP_404_NOT_FOUND,
        detail='{type(model)} not found'
      )

  return get_model_from_id


@app.get('/councils/{id}')
def get_council(council: Council = Depends(get_model_from_path(crud.council)):
  pass
  • Related