Introduction
Hello! In our last lesson, we successfully created our first "pre-tool hook"—a custom FastAPI dependency named validate_order_id_format that checks if an order ID has the correct format. We applied it using the dependencies argument in the @app.get decorator, which is a great way to attach "fire-and-forget" validation that runs for its side effects (like raising an error).
Today, we're going to build on that by learning a more powerful and common way to apply the very same dependency. Instead of attaching it to the decorator, we'll inject it directly into our tool's function signature. This approach is not just an alternative; it's the foundation for more advanced patterns where dependencies need to pass data or results into your tool's logic.
Your experience with Rasa Pro actions, which receive a tracker and dispatcher object to work with, provides a good mental model here. Just as an action needs specific inputs to do its job, this new dependency pattern allows us to explicitly declare what our tool function needs to execute.
1. Two Ways to Apply a Dependency
FastAPI offers two primary ways to apply a dependency to a path operation, and choosing the right one depends on your goal:
-
In the Decorator:
dependencies=[Depends(my_dep)]- Use Case: Ideal for dependencies that don't return a value your endpoint function needs. Examples include logging a request, checking an API key, or—as we did last lesson—running validation that only raises an exception on failure.
- Analogy: This is like a security guard at the door. They either let you in or they don't; they don't hand you anything on your way in.
-
In the Function Signature:
def my_endpoint(value: Annotated[T, Depends(my_dep)])- Use Case: Essential when the dependency performs a task and returns a result that your endpoint logic needs to use. This could be fetching a user object from a database, processing an input, or returning a validated and sanitized value.
- Analogy: This is like a concierge. You give them a request (e.g., a ticket number), they validate it and process it, and then they hand you back something you need (e.g., your event pass).
Today, we will refactor our get_order_details tool to use the second pattern. It's a subtle but significant change that makes our code more explicit and extensible.
2. Injecting a Validation Dependency
To see this pattern in action, let's study a clear, practical example. The following article demonstrates exactly how to create a dependency that validates a path parameter and is then applied directly in the function signature.
Fastapi Depends with parameters and arguments - Hrekov
The article 'Fastapi Depends with parameters and arguments' from Hrekov provides excellent, concise examples of dependency injection patterns. We'll focus on the very first one.
Please read the section titled 'Request Input Parameters (Path/Query/Header)'. Pay close attention to how validate_item_id is defined and then used inside the read_item function's signature via the ValidItemID type alias. This is precisely the pattern we are about to implement.
As you saw in the article, the dependency validate_item_id doesn't just raise an error; it also returns the item_id on success. This allows the path operation function read_item to receive the validated value.
3. Refactoring Our Code
Let's apply this pattern to our own validate_order_id_format dependency and the get_order_details tool.
Step 1: Update the Dependency to Return a Value
First, modify validate_order_id_format in main.py so that it returns the order_id if validation succeeds.
# main.py
# ... inside your 'Custom Validation Dependencies' section
async def validate_order_id_format(order_id: str):
"""
Dependency to validate that the order_id starts with 'ORD'
and is followed by digits. Returns the validated order_id on success.
"""
if not order_id.startswith("ORD") or not order_id[3:].isdigit():
raise HTTPException(
status_code=400, # Bad Request
detail="Invalid order_id format. Must start with 'ORD' followed by digits."
)
# If validation passes, return the validated ID.
return order_id
Step 2: Apply the Dependency in the Function Signature
Now, we'll change how the get_order_details endpoint uses this dependency. This involves two small changes: removing the dependency from the decorator and adding it to a function parameter's type annotation.
Make sure you have Annotated imported from the typing module at the top of your main.py.from typing import Optional, Dict, Any, Annotated
# main.py
# ... in the 'Tool Endpoints' section
@app.get(
"/orders/{order_id}",
operation_id="get_order_details",
description="Fetches detailed information for a specific order by its ID.",
tags=["orders"],
response_model=Order,
# REMOVE this line: dependencies=[Depends(validate_order_id_format)],
)
async def get_order_details(
# APPLY the dependency here instead
order_id: Annotated[str, Depends(validate_order_id_format)]
) -> Dict[str, Any]:
"""
Example endpoint to simulate fetching order data.
This code will only run if the dependency validation passes.
"""
# The rest of the function body remains the same.
# It still uses the `order_id` parameter, which now holds the validated value.
if order_id == "ORD12345":
return {
"order_id": order_id,
"customer_id": 101,
"status": "shipped",
"total_amount": 199.99,
"currency": "USD",
}
raise HTTPException(status_code=404, detail="Order not found")
Let's break down this refactoring:
- We removed
dependencies=[Depends(validate_order_id_format)]from the@app.getdecorator. - We changed the
order_idparameter fromorder_id: strtoorder_id: Annotated[str, Depends(validate_order_id_format)].
Now, when a request comes to /orders/{some_id}, FastAPI sees the Annotated type hint and does the following:
- It runs the
validate_order_id_formatdependency, passing the{some_id}value to itsorder_idargument. - If the dependency raises an
HTTPException, the request is stopped and the error is returned. - If the dependency succeeds, it returns the validated
order_id. FastAPI then assigns this returned value to theget_order_detailsfunction'sorder_idparameter. - Finally, the body of
get_order_detailsis executed.
This pattern is cleaner and more explicit about the data flow. The visual below reinforces this concept of injecting a dependency to secure a route.

4. Verifying the Refactored Logic
Even though we've changed the implementation, the user-facing behavior should be identical. Let's confirm this.
- Ensure your FastAPI server is running with the latest changes (
uv run uvicorn main:app --reload). - Open the MCP Inspector and connect to your server (
http://localhost:8000/mcp) with the correctAuthorizationheader.
- Test 1 (Valid): Execute the
get_order_detailstool withorder_idset toORD12345.- Expected Result: A successful
200 OKresponse with the order details.
- Expected Result: A successful
- Test 2 (Invalid): Execute the tool with
order_idset toinvalid-id.- Expected Result: A
400 Bad Requesterror with the detail message:"Invalid order_id format. Must start with 'ORD' followed by digits."
- Expected Result: A
Your pre-tool hook is working exactly as before, but it's now implemented using a more powerful and idiomatic FastAPI pattern.
Conclusion
In this lesson, you refactored your pre-tool hook to use a more versatile dependency injection pattern. This small change from the decorator to the function signature opens up a new world of possibilities for creating more intelligent tools.
Key Takeaways:
- Dependencies can be applied in the decorator (for side-effects) or in the function signature (to provide values).
- Applying a dependency in the function signature using
Annotated[<type>, Depends(<dependency>)]is the standard way to run validation and pass the sanitized result into your endpoint logic. - For this pattern to work, the dependency function should
returnthe validated value upon successful execution.
In our next lesson, we will build directly on this new pattern. We'll create a "gating" dependency that performs a more complex check using business logic—going beyond simple format validation to decide if a tool should be allowed to execute at all.