FastAPI - Beginner to Pro / Project 2- Move Fast With FastAPI

Data Validation Path Parameters

πŸ›‘οΈ 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_id must be greater than 0

  • If 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 Response

0

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 Response

0 and 10

You should receive:

HTTP 422 β€” Unprocessable Entity

Any 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