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?
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
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.
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/
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?
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
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.
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/