FastAPI conflict path parameter in endpoint – good practices?

Question:

I am creating 2 GET methods for a resource student using FastAPI. I’m looking to GET a student in 2 ways: by student_id or by student_name.

The issue is that, I initially created the 2 endpoints as follows

@app.get("/student/{student_name}", response_model=schemas.Student, status_code=200)
def get_student_by_name(student_name: str, db: Session = Depends(get_db)):
    db_student = crud.get_student_by_name(db, student_name)
    if db_student is None:
        raise HTTPException(status_code=404, detail="Student not found")
    return db_student


@app.get("/student/{student_id}", response_model=schemas.Student, status_code=200)
def get_student_by_id(student_id: int, db: Session = Depends(get_db)):
    db_student = crud.get_student_by_id(db, student_id)
    if db_student is None:
        raise HTTPException(status_code=404, detail="Student not found")
    return db_student

The problem is that the endpoint names are conflicting with each other, it is both /student followed by a parameter and only one of them could work – in this case only /student/{student_name} because it is defined in the front. So I came up with this simple workaround by adding a bit more to the endpoint names:

@app.get("/student/{student_name}", response_model=schemas.Student, status_code=200)
def get_student_by_name(student_name: str, db: Session = Depends(get_db)):
    db_student = crud.get_student_by_name(db, student_name)
    if db_student is None:
        raise HTTPException(status_code=404, detail="Student not found")
    return db_student


@app.get("/student/byid/{student_id}", response_model=schemas.Student, status_code=200)
def get_student_by_id(student_id: int, db: Session = Depends(get_db)):
    db_student = crud.get_student_by_id(db, student_id)
    if db_student is None:
        raise HTTPException(status_code=404, detail="Student not found")
    return db_student

I added /byid to the endpoint name of the get_student)by_id method. While both endpoints could work now, I am wondering if this is considered a good practice? WHat would be the best practice when one resource needed to be queried with a single path parameter to differentiate the endpoint names?

Asked By: Daniel Qiao

||

Answers:

I will do something like this

@app.get("/student/{student_id}", response_model=schemas.Student, status_code=200)
def get_student(student_id: str, db: Session = Depends(get_db)):
    db_student = crud.get_student_by_id(db, student_id)
    if db_student is None:
        raise HTTPException(status_code=404, detail="Student not found")
    return db_student


# use search criterias as query params
@app.get("/student/", response_model=List[schemas.Student], status_code=200)
def get_students(student_name: string = None, db: Session = Depends(get_db)):
    # Query inside your crud file
    query = db.query(Student)
    if student_name:
        # if you want to search similar items
        query = query.filter(Student.name.like(f"%{student_name}%"))
        # if you want to search an exact match
        query = query.filter(Student.name == student_name)
    
    return query.all()

With this your code will be a little bit more open to future changes, I will only use a url param when searching by id, any other search criteria can be handled as a filter parameter using query params

Answered By: erick.chali

Whether best practice or not is subjective.

If I want to ensure compatibility with the web framework I am porting over to FastAPI, and it displayed the original behaviour then there is a way in which I could go about replicating that.

@app.get("/student/{student_id}",
         response_model=schemas.Student,
         status_code=200)
def get_student_by_id(
        student_id: int = Path(
            title="The ID of the student to get"),
        db: Session = Depends(get_db)):
    db_student = crud.get_student_by_id(db, student_id)
    if db_student is None:
        raise HTTPException(status_code=404, detail="Student not found")
    return db_student
...


@app.get("/student/{student_name}",
         response_model=schemas.Student,
         status_code=200)
def get_student_by_name(
        student_name: str,
        db: Session = Depends(get_db)):
    db_student = crud.get_student_by_name(db, student_name)
    if db_student is None:
        raise HTTPException(status_code=404, detail="Student not found")
    return db_student

https://fastapi.tiangolo.com/tutorial/path-params-numeric-validations/

With FastAPI’s emphasis on typing, and documentation, it makes sense to me, that the same path prefix should intelligently route to the more specific, int-convertable, especially if the more specific comes first.

If I can forego stronger documentation and validation I can cover both bases like this:

@app.get("/student/{student_name_or_id}",
         response_model=schemas.Student,
         status_code=200)
async def get_student(
        student_name_or_id: str = Path(
            title="The ID or name of the student to get"),
        db: Session = Depends(get_db)):
    if student_name_or_id.isdigit():
        db_student = crud.get_student_by_id(db, int(student_name_or_id))
    else:
        db_student = crud.get_student_by_name(db, student_name_or_id)
    if db_student is None:
        raise HTTPException(status_code=404, detail="Student not found")
    return db_student

https://fastapi.tiangolo.com/tutorial/path-params/#path-convertor

Your existing logic for validating if the argument is an int, or student name, then validate those or return a suitable error. I see you already have this logic so that isn’t the biggest drawback.

I used this approach a lot initially. Having learned the value of the included OpenAPI documentation I would prefer separate functions. Sometimes it is better to maintain your sites links. WordPress for example can serve pages simultaneously at /2022/08 and /python/faster-fastapi, one using date, the other category and slug.

Answered By: John

Been having this same issue for hours now, and just found a perfect solution by pure dumb luck while experimenting:

@app.get("/student/{student_name:str}", response_model=schemas.Student, status_code=200)
def get_student_by_name(student_name: str, db: Session = Depends(get_db)):
    ...

@app.get("/student/{student_id:int}", response_model=schemas.Student, status_code=200)
def get_student_by_id(student_id: int, db: Session = Depends(get_db)):
    ...

Adding these "convertors" to your path will route your requests to the correct endpoint based on the specified type 🙂

I was able to find the docs for this in Starlette, which I’m not too familiar with, but FastAPI is built on top of it. https://www.starlette.io/routing/

Answered By: Darren Matthew