FastAPI - Beginner to Pro / Project 1 - FastAPI Request Method Logic

Get Book By Author

πŸ“š Get All Books by a Specific Author

🎯 Retrieve all books written by a specific author using:

  • A Path Parameter

  • A Query Parameter


🎯 Expected Behavior

Here’s what our endpoint should do:

  • πŸ‘‰ If we type Author Two β†’ return 2 books

  • πŸ‘‰ If we type Author One β†’ return 1 book

  • πŸ‘‰ If we type a non-existent author β†’ return 0 books

We will first implement the Path Parameter solution.


πŸ›£οΈ Approach 1 β€” Using a Path Parameter

We will create a new endpoint that accepts the author's name directly in the URL path.

πŸ—‚οΈ books.py

# endpoint that accepts the author's name directly in the URL path.
@app.get("/books/byauthor/{author}")
async def read_books_by_author_path(author: str):
    books_to_return = []
    for book in BOOKS:
        if book.get('author').casefold() == author.casefold():
            books_to_return.append(book)
    return books_to_return

πŸ” Step-by-Step Explanation

πŸ‘‰ 1. Define the Route

"/books/byauthor/{author}"
  • {author} is a dynamic path parameter

  • Whatever value is passed in that position becomes the author variable

πŸ‘‰ 2. Define the Function Parameter

async def read_books_by_author_path(author: str): 
  • author is typed as a string

  • FastAPI automatically extracts it from the URL

πŸ‘‰ 3. Create a Filtered List

books_to_return = []

We create an empty list to store matching books.

πŸ‘‰ 4. Loop Through BOOKS

for book in BOOKS:

We iterate through all books.

πŸ‘‰ 5. Case-Insensitive Comparison

book.get('author').casefold() == author.casefold()

🧠 Why use casefold()?

  • Ensures case-insensitive comparison

  • "Author Two" and "author two" will match

πŸ‘‰ 6. Return the Results

return books_to_return

We return all books that match the author.

▢️ Running the Application

Open your terminal and start the server:

uvicorn books2:app --reload

🌐 Testing in the Browser

Steps:

  1. Go to /docs

  2. Locate: GET /books/byauthor/{author}

  3. Click Try it out

  4. Enter Author Two

  5. Click Execute

βœ” You should see 2 books

Try:

  • Author One β†’ 1 book

  • Unknown Author β†’ 0 books (an empty list)


πŸ”„ Approach 2 β€” Switching to a Query Parameter

Now, let’s modify the endpoint to use a query parameter instead.

πŸ‘‰ To do this, remove {author} from the path.

When there is no dynamic path parameter, FastAPI automatically treats function parameters as query parameters.

After modifying:

@app.get("/books/byauthor/")
async def read_books_by_author_path(author: str):

Now author becomes a query parameter.

❌ Unexpected Error β€” β€œUnprocessable Entity”

If you refresh Swagger and test:

/books/byauthor/?author=Author Two

You may see:

422 Unprocessable Entity

πŸ€” Why is this happening?

This brings us to a very important concept.


⚠️ Critical Concept β€” ORDER MATTERS in FastAPI

This endpoint:

@app.get("/books/byauthor/")

Conflicts with this one:

@app.get("/books/{book_author}/")
async def read_author_and_category(book_author: str, category: str):

🧠 Why the Conflict Happens

FastAPI reads routes from top to bottom.

When you define:

@app.get("/books/{book_author}/") <---

It matches ANY dynamic value after /books/.

So when you type:

/books/byauthor

FastAPI thinks:

book_author = "byauthor"

And expects another parameter (category).

When it doesn't receive it:

❌ It throws a validation error.


πŸ”„ Fix β€” Fixing the Route Order

To solve this:

πŸ‘‰ Move the more specific route to the top.

❌ Before (Incorrect Order)

πŸ—‚οΈ books.py

@app.get("/books/{book_author}/")
...
@app.get("/books/byauthor/{author}")

βœ… After (Correct Order)

πŸ—‚οΈ books.py

@app.get("/books/byauthor/")
...
@app.get("/books/{book_author}/")

🧠 Rule of Thumb:

Always place more specific endpoints above more generic ones

🌐 Testing Again in the Browser

Now:

  • GET /books/byauthor/{author} works

  • GET /books/{book_author}/ still works

  • Filtering by category also works

Everything functions correctly.


🧠 Why This Matters

FastAPI processes routes chronologically.

If a general route appears first, it can:

  • Intercept requests

  • Consume dynamic values

  • Cause validation failures

This is especially important when working in a single file.


πŸ“˜ Path vs Query Parameter

Type

Example

When to Use

Path Parameter

/books/byauthor/Author Two

Required identifier

Query Parameter

/books/byauthor?author=Author Two

Optional filtering


🚨 FastAPI Rule to Remember

Best Practice

Reason

Place specific routes first

Prevent route conflicts

Place generic routes later

Avoid unintended matches

Always test after reordering

Ensure correct routing


πŸ—‚οΈ books.py

from fastapi import FastAPI, Body

app = FastAPI()

BOOKS = [
    {"title": "Title One", "author": "Author One", "category": "science"},
    {"title": "Title Two", "author": "Author Two", "category": "science"},
    {"title": "Title Three", "author": "Author Three", "category": "history"},
    {"title": "Title Four", "author": "Author Four", "category": "math"},
    {"title": "Title Five", "author": "Author Five", "category": "math"},
    {"title": "Title Six", "author": "Author Two", "category": "math"}
]

@app.get("/books")
async def read_all_books():
    return BOOKS

@app.get('/books/{book_title}')
async def read_book(book_title: str):
    for book in BOOKS:
        if book.get('title').casefold() == book_title.casefold():
            return book

@app.get("/books/")
async def read_category_by_query(category: str):
    books_to_return = []
    for book in BOOKS:
        if book.get('category').casefold() == category.casefold():
            books_to_return.append(book)
    return books_to_return

@app.get("/books/byauthor/")
async def read_books_by_author_path(author: str):
    books_to_return = []
    for book in BOOKS:
        if book.get('author').casefold() == author.casefold():
            books_to_return.append(book)
    return books_to_return

@app.get("/books/{book_author}/")
async def read_author_and_category(book_author: str, category: str):
    books_to_return = []
    for book in BOOKS:
        print('book', book)
        if book.get('author').casefold() == book_author.casefold() and \\
                book.get('category').casefold() == category.casefold():
            books_to_return.append(book)
    return books_to_return

@app.post("/books/create_book")
async def create_book(new_book=Body()):
    BOOKS.append(new_book)

@app.put("/books/update_book")
async def update_book(updated_book=Body()):
    for i in range(len(BOOKS)):
        if BOOKS[i].get('title').casefold() == \\
                updated_book.get('title').casefold():
            BOOKS[i] = updated_book

@app.delete("/books/delete_book/{book_title}")
async def delete_book(book_title: str):
    for i in range(len(BOOKS)):
        if BOOKS[i].get('title').casefold() == book_title.casefold():
            BOOKS.pop(i) 
            break