Shared Dependencies for Data Validation
Learn to create shared dependencies for data validation in FastAPI. Centralize validation logic, reuse it across endpoints, and handle edge cases. Practical walkthrough included.
Focus: creating shared dependencies for data validation
You’ve built a few FastAPI endpoints, and you’re starting to notice a pattern: every route that accepts a query parameter or a request body repeats the same validation logic. Maybe you check page and limit for pagination, or you validate an Authorization header by calling the same helper function over and over. Copy-pasting validation code across endpoints is a maintenance nightmare — one small change to the rules means hunting down every copy. FastAPI’s dependency injection system gives you a cleaner path: create shared dependencies for data validation once, then plug them into any route that needs them. By the end of this lesson, you’ll be able to centralize validation logic into reusable dependencies, keep your endpoints slim, and handle edge cases like a pro.
The problem this lesson solves
Every FastAPI endpoint that takes user input needs validation. You might check that a page parameter is a positive integer, that an email address is well-formed, or that a request body conforms to business rules. When you write that validation inline in each route, you end up with repetitive code that is hard to maintain.
Consider this typical anti-pattern:
from fastapi import FastAPI, Query, HTTPException
app = FastAPI()
@app.get("/items")
async def list_items(page: int = Query(1, ge=1), limit: int = Query(10, ge=1, le=100)):
# some logic
return {"page": page, "limit": limit}
@app.get("/users")
async def list_users(page: int = Query(1, ge=1), limit: int = Query(10, ge=1, le=100)):
# duplicated validation!
return {"page": page, "limit": limit}
Now imagine you need to change the maximum limit from 100 to 200. You’d have to edit every endpoint that uses it. In a real app with dozens of routes, that’s error-prone and a waste of time. Worse, if you forget one spot, you have inconsistent behavior across your API.
The solution is shared dependencies for data validation. Instead of repeating validation logic in each route, you define a dependency function that performs the validation once, and then you use it in any route that needs it. FastAPI will automatically call that dependency, validate the input, and pass the result to your endpoint.
Core concept / mental model
Think of a dependency as a gatekeeper that stands at the entrance to your endpoint. When a request arrives, FastAPI first runs all the dependencies that your route declares. The dependency receives the raw input (query parameters, headers, body, etc.), checks it against your validation rules, and either returns a cleaned, validated value or raises an error that FastAPI turns into a 422 Unprocessable Entity (or a custom error).
In simple terms, dependencies are functions (or callables) that FastAPI can inject into your endpoints. They can have their own parameters, and FastAPI will resolve those too. This means you can build a chain of dependencies — a pagination dependency depends on a database session, for example.
Here’s the mental model:
- Define a Python function that takes the input parameters (with FastAPI annotations) and returns a validated object.
- Declare that function as a dependency in your route (either in the function signature with
Depends()or in adependencieslist). - Reuse it across multiple routes.
The result: your endpoint code becomes concise and focused on business logic, while validation logic lives in one place — easy to test and easy to change.
Definitions you’ll need
- Dependency: A callable that FastAPI resolves and injects into your route. It can return a value, which is passed to the endpoint as an argument with the same name.
Depends(): The function you use to tell FastAPI that a route parameter should come from a dependency.- Shared dependency: A dependency that is used in multiple routes, often defined in a separate module.
How it works step by step
- Create a dependency function. This function can take any parameters that FastAPI understands (query params, headers, body parts, etc.) and must return a value that will be available to your endpoint.
- Add validation logic inside the function. Use FastAPI’s
Query,Path,Header, orBodyto enforce constraints, or raiseHTTPExceptionfor custom checks. - Use the dependency in a route. In your route function signature, add a parameter with
Depends(your_dependency_function). FastAPI will call it before your endpoint runs and pass the returned value to the matching parameter name. - Reuse across routes. Import your dependency into any route module and use
Depends()wherever needed.
Let’s look at a simple pagination dependency:
from typing import Tuple
from fastapi import Query, Depends
def pagination_params(page: int = Query(1, ge=1), limit: int = Query(10, ge=1, le=100)) -> Tuple[int, int]:
return page, limit
@app.get("/items")
async def list_items(pagination: Tuple[int, int] = Depends(pagination_params)):
page, limit = pagination
return {"page": page, "limit": limit}
But wait — returning a tuple can be confusing. A better approach is to return a small class or a Pydantic model. That brings us to the hands-on section.
Hands-on walkthrough
Let’s build a realistic example: a blog API where you validate query parameters for pagination and also validate an Authorization header to ensure it’s a valid token (here we check that it looks like a token, but in real life you’d verify against a database or signature). You’ll create a shared dependency module and use it in two different endpoints.
Step 1: Create the dependency module
Create a file dependencies.py:
from fastapi import Query, Header, HTTPException
from pydantic import BaseModel
from typing import Optional
class PaginationParams(BaseModel):
page: int
limit: int
def get_pagination(
page: int = Query(1, ge=1, description="Page number"),
limit: int = Query(10, ge=1, le=100, description="Items per page"),
) -> PaginationParams:
return PaginationParams(page=page, limit=limit)
def require_token(authorization: Optional[str] = Header(None)) -> str:
if not authorization:
raise HTTPException(status_code=401, detail="Authorization header missing")
if not authorization.startswith("Bearer "):
raise HTTPException(status_code=401, detail="Invalid token format")
token = authorization.removeprefix("Bearer ")
if len(token) < 20:
raise HTTPException(status_code=401, detail="Token too short")
return token
Step 2: Use the shared dependencies in your routes
Create main.py:
from fastapi import FastAPI, Depends
from dependencies import get_pagination, require_token, PaginationParams
app = FastAPI()
@app.get("/posts")
async def list_posts(
pagination: PaginationParams = Depends(get_pagination),
token: str = Depends(require_token),
):
# Here you would actually fetch posts from a database
return {"page": pagination.page, "limit": pagination.limit, "token": token}
@app.get("/comments")
async def list_comments(
pagination: PaginationParams = Depends(get_pagination),
token: str = Depends(require_token),
):
# Another endpoint, same validation!
return {"page": pagination.page, "limit": pagination.limit, "token": token}
Now both endpoints share the same pagination and token validation logic. If you decide to change the token length rule or the maximum limit, you edit dependencies.py once, and every route that uses those dependencies is updated automatically.
Step 3: Test with FastAPI’s interactive docs
Run uvicorn main:app --reload, open http://localhost:8000/docs, and try:
/posts?page=1&limit=5with a validAuthorization: Bearer abcdefghijklmnopqrstuvwxyzheader (must be at least 20 chars)./posts?page=0— you’ll get a422because page must be >= 1.- Missing Authorization header — you’ll get
401.
Expected output for a successful request:
{"page":1,"limit":5,"token":"abcdefghijklmnopqrstuvwxyz"}
Step 4: Add a dependency that depends on another dependency
You can create chains. For example, a dependency that requires a valid admin token:
from fastapi import Depends, HTTPException
from dependencies import require_token
def require_admin(authorization: str = Depends(require_token)) -> str:
# In reality you'd decode the JWT and check roles
if not authorization.startswith("admin-"):
raise HTTPException(status_code=403, detail="Admin privileges required")
return authorization
Now use Depends(require_admin) in admin-only routes.
Compare options / when to choose what
There are several ways to handle shared validation logic in FastAPI. Here’s a comparison to help you decide:
| Approach | Pros | Cons | Best when |
|---|---|---|---|
Inline with Query/Header |
Simple, good for one-off routes | Repetitive, hard to maintain | A single endpoint with unique validation |
| Shared dependency function | Reusable, testable, centralizes logic | Slightly more setup | Validation that many endpoints share |
| Pydantic models | Type safety, validation in one place | Can’t be used for query params in a single object (unless you use Query in a dependency) |
Request body validation |
Custom classes with __call__ |
Can encapsulate state (e.g., database session) | Overkill for simple checks | When you need a dependency class with internal state |
For query parameter validation (like pagination), a dependency function returning a Pydantic model is the cleanest. For request body validation, Pydantic models alone are usually enough. For headers or global checks (like auth), dependencies excel.
Pro tip: Always separate dependencies into their own module (e.g.,
dependencies.py) so you can import them anywhere without circular imports.
Troubleshooting & edge cases
Here are common pitfalls and how to fix them:
- Dependency returns a tuple, but your endpoint expects a dict: FastAPI maps the dependency’s return value to the parameter name. If your dependency returns a tuple, you might not get the expected structure. Use a Pydantic model or a named tuple instead.
- You get
422errors even though your logic seems correct: Double-check that yourQueryconstraints are satisfied. For example, if you setle=100and the client sendslimit=101, FastAPI returns422. This is intended — rely on FastAPI’s validation messages. - Dependency with optional header: If you use
Header(None)and the header is missing, the value will beNone. Check for it before using it, or raiseHTTPExceptionas we did. - Circular imports: When you split dependencies into separate files, avoid importing from the main app module. Keep dependencies self-contained.
- Async vs sync: Dependencies can be async. If you need to perform I/O (like database queries), make your dependency
async def. If it’s just validation, a sync function is fine.
What you learned & what's next
In this lesson, you learned how to create shared dependencies for data validation in FastAPI. You saw how to:
- Recognize the pain of repeated inline validation.
- Define a dependency function that validates query parameters, headers, or other inputs.
- Use
Depends()to inject that validation into multiple routes. - Build dependency chains for more complex checks.
- Compare dependency-based validation with other approaches.
- Troubleshoot common issues like
422errors and import problems.
You now have a powerful tool in your FastAPI toolkit. Next in this track, you’ll learn how to use dependencies for common security patterns like role‑based access control and how to test your FastAPI app with pytest. These skills will build on the shared dependency pattern you just mastered.
Practice recap
Create a new dependency that validates a q query parameter to be a non-empty string with a max length of 50 characters. Use it in two different endpoints in a small app, then test with invalid inputs to see FastAPI’s validation messages. This solidifies the pattern before moving on to authentication dependencies.
Common mistakes
- Returning a tuple or dict from a dependency and forgetting to unpack it correctly — use a Pydantic model for clarity.
- Forgetting to add
Depends()when declaring the dependency in the route signature, which causes FastAPI to treat it as a query parameter. - Using optional headers without a default or without checking for
None, leading toNoneTypeerrors when the header is absent. - Placing dependency functions in the same module as routes, causing circular imports when you later split modules.
Variations
- Using a dependency class with
__call__to maintain state (e.g., a database session helper). - Using
dependencies=[...]in the route decorator to run dependencies without capturing their return value (e.g., for auth side-effects). - Leveraging FastAPI’s
Depends()with Pydantic model fields for request body validation, but that’s only for body, not query parameters.
Real-world use cases
- Centralize pagination and filtering parameters across multiple list endpoints in a REST API.
- Create a shared authentication dependency that validates JWT tokens for all protected routes.
- Build a common tenant-ID validation dependency for multi-tenant SaaS APIs.
Key takeaways
- Shared dependencies eliminate duplicate validation code and centralize changes.
- Dependencies are callables that FastAPI resolves before your endpoint runs.
- Use
Depends()to inject a dependency’s return value into your route. - Chain dependencies to build more complex checks (like admin auth).
- Always use Pydantic models or named types for dependency return values to keep code readable.
- Separate dependencies into their own module to avoid circular imports.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.