π‘οΈ Adding Validation to Path Parameters
So far, most of our validation has been handled inside the Pydantic BookRequest model.
π That means request bodies are validated.
But what about path parameters like:
/books/{book_id}At this stage, those parameters are only type-checked β not fully validated.
Letβs fix that.
π§ͺ Understand the Problem (No Path Validation)
Here is our current endpoint:
@app.get("/books/{book_id}") # π current endpoint
async def read_book(book_id: int):
for book in BOOKS:
if book.id == book_id:
return bookβ Whatβs Missing Here?
There are two important backend problems:
1οΈβ£ What if the book does not exist?
2οΈβ£ What if someone sends:
/books/0
/books/-5π Right now, those values are allowed.
Although book_id is typed as int, we are not enforcing:
Positive values
Acceptable numeric ranges
This weakens our API validation layer.
π§ Step 1: Import Path for Parameter-Level Validation
FastAPI allows us to validate path parameters directly, just like we validate fields in Pydantic.
π Import Path from FastAPI:
from typing import Optional
from fastapi import FastAPI, Path π
from pydantic import BaseModel, Fieldπ§ Notice the Similarity
In Pydantic, we do:
rating: int = Field(gt=0, lt=6)For path parameters, we do:
book_id: int = Path(gt=0)π Same idea.
π Different layer.
π Step 2: Add Validation to the GET Endpoint
Now letβs update the route:
@app.get("/books/{book_id}")
async def read_book(book_id: int = Path(gt=0)):
for book in BOOKS:
if book.id == book_id:
return bookπ What Does Path(gt=0) Do?
It enforces:
book_idmust be greater than 0If not β FastAPI automatically returns:
HTTP 422 β Unprocessable Entityπ Test in the Browser
π Open Swagger UI
Try the following book idβs:
6
You should receive:
200 OK Successful Response0
You should receive:
HTTP 422 β Unprocessable Entityπ And your function will NOT execute.
Thatβs important.
Validation happens before business logic runs.
π Apply Validation to Other Endpoints
We should apply the same principle to DELETE:
@app.delete("/books/{book_id}")
async def delete_book(book_id: int = Path(gt=0)):
for i in range(len(BOOKS)):
if BOOKS[i].id == book_id:
BOOKS.pop(i)
breakπ Test in the Browser
π Open Swagger UI
Try the following book idβs:
1
You should receive:
200 OK Successful Response0 and 10
You should receive:
HTTP 422 β Unprocessable EntityAny time you accept:
Path parameters
Query parameters
Request bodies
π You should validate them.
This prevents:
Invalid operations
Unexpected errors
Logical inconsistencies
ποΈ books2.py
from typing import Optional
from fastapi import FastAPI, Path
from pydantic import BaseModel, Field
app = FastAPI()
class Book:
id: int
title: str
author: str
description: str
rating: int
published_date: int
def __init__(self, id, title, author, description, rating, published_date):
self.id = id
self.title = title
self.author = author
self.description = description
self.rating = rating
self.published_date = published_date
class BookRequest(BaseModel):
id: Optional[int] = Field(description="ID is not required", default=None)
title: str = Field(min_length=3)
author: str = Field(min_length=1)
description: str = Field(min_length=1, max_length=100)
rating: int = Field(gt=1, lt=6)
published_date: int = Field(gt=1999, lt=2031)
model_config = {
"json_schema_extra": {
"example": {
"title": "A new book",
"author": "Name of the author",
"description": "The description of the book",
"rating": 5,
"published_date": 2029
}
}
}
BOOKS = [
Book(1, "Computer Science Pro", "codingwithrobby", "A very nice book", 3, 2030),
Book(2, "Be Fast with FastAPI", "codingwithrobby", "A great book", 5, 2030),
Book(3, "Master Endpoints", "codingwithrobby", "An awesome book", 5, 2029),
Book(4, "HP1", "Author 1", "Book Description", 2, 2028),
Book(5, "HP2", "Author 2", "Book Description", 3, 2027),
Book(6, "HP3", "Author 3", "Book Description", 1, 2026)
]
@app.get("/books")
async def read_all_books():
return BOOKS
@app.post("/create_book")
async def create_book(book_request: BookRequest):
new_book = Book(**book_request.model_dump())
BOOKS.append(find_book_id(new_book))
@app.get("/books/{book_id}")
async def read_book(book_id: int = Path(gt=0)):
for book in BOOKS:
if book.id == book_id:
return book
@app.get("/books/")
async def read_book_by_rating(book_rating: int):
books_to_return = []
for book in BOOKS:
if book.rating == book_rating:
books_to_return.append(book)
return books_to_return
@app.put("/books/update_book")
async def update_book(book: BookRequest):
for i in range(len(BOOKS)):
if BOOKS[i].id == book.id:
BOOKS[i] = book
@app.delete("/books/{book_id}")
async def delete_book(book_id: int = Path(gt=0)):
for i in range(len(BOOKS)):
if BOOKS[i].id == book_id:
BOOKS.pop(i)
break
@app.get("/books/publish/")
async def read_books_by_publish_date(published_date: int):
books_to_return = []
for book in BOOKS:
if book.published_date == published_date:
books_to_return.append(book)
return books_to_return
def find_book_id(book: Book):
book.id = 1 if len(BOOKS) == 0 else BOOKS[-1].id + 1
return book