When many endpoints share the same q, skip, and limit parameters, do not repeat them in every function. Group them into a class dependency: FastAPI creates an instance of that class from the query parameters on each request, and every endpoint only needs to declare Depends(CommonQueryParams). This applies whenever two or more endpoints share the same pagination or filtering parameter set.
The Problem
A list API usually repeats the same three parameters on every endpoint: a search keyword, a start offset, and a page size. Writing these three parameters by hand in each function is verbose and prone to drift: one endpoint defaults limit to 100, another to 20, and when you want to add a new parameter you must edit every single place. Over time the endpoints’ parameter sets stop matching, and the OpenAPI docs get harder to read.
@app.get("/items/")def read_items(q: str | None = None, skip: int = 0, limit: int = 100): ...@app.get("/users/")def read_users(q: str | None = None, skip: int = 0, limit: int = 100): ...
The Solution
FastAPI accepts any “callable” as a dependency, and a class is a callable: calling a class runs its __init__ method. So you can declare the parameters right inside __init__, and FastAPI will read them like ordinary query parameters and hand the ready-made instance to the endpoint.
from fastapi import Depends, FastAPI, Queryclass CommonQueryParams: def __init__( self, q: str | None = None, skip: int = 0, limit: int = Query(default=100, le=100), ): self.q = q self.skip = skip self.limit = limitapp = FastAPI()ITEMS = [{"name": "ao"}, {"name": "quan"}, {"name": "mu"}, {"name": "giay"}]@app.get("/items/")def read_items(commons: CommonQueryParams = Depends(CommonQueryParams)): result = ITEMS[commons.skip : commons.skip + commons.limit] if commons.q: result = [item for item in result if commons.q in item["name"]] return {"items": result}
This is better in three ways. First, the parameter set is defined once, so defaults can no longer drift between endpoints. Second, reading commons.skip or commons.limit in an endpoint is far more expressive than digging individual keys out of a dict. Third, all the familiar validation still works: Query(default=100, le=100) inside __init__ still makes a limit over 100 return a 422 error before your code runs. If your project uses the Annotated syntax, Annotated[CommonQueryParams, Depends(CommonQueryParams)] gives the same result.
Apply It to Your Project
Put this class in its own module, for example dependencies.py, then import it for every router that needs pagination and filtering. When you need a variant — say an admin page that allows a limit up to 500 — do not edit the shared class and affect old endpoints: create a subclass or a separate class inheriting that logic. Keep the dependency class lean, concerned only with grouping parameters and validation; business logic stays in the endpoint or service, so the class remains genuinely reusable for a long time.
Related Posts
- FastAPI: Understand Depends Caching — When to Use use_cache=False in 5 Minutes
- FastAPI: Bundle a Dozen Query Params into One Pydantic Model — Centralized Validation in 5 Minutes
- FastAPI: Attach Request-IDs and Measure Processing Time for Every Request with Custom Middleware in 5 Minutes
Frequently Asked Questions
How is a class dependency different from a function dependency?
Essentially it is not: FastAPI only needs a callable to invoke on each request. A function returns a dict or an object, a class returns an instance of itself. The class wins by grouping parameters into clearly named attributes and by being easy to extend through inheritance.
Is a new instance created for each request?
Yes. FastAPI creates a new instance of the class for each request, just as it calls a function dependency again on each request. So do not use instance attributes to store state shared between requests.
Can I use the Annotated syntax?
Yes. You write commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] and receive exactly the same instance as with the default syntax. FastAPI’s official documentation recommends the Annotated form when possible.
Does validation like Query() run inside __init__?
Yes. The parameters of __init__ are processed by FastAPI exactly like the parameters of a function dependency, so Query, Header, or Path placed there all validate and return 422 errors as usual.
💬 Have you tried this in your own project? If you hit an error or have a cleaner way, leave a comment below — I read them all and will fold the best ideas into the post.
References
- FastAPI — Classes as Dependencies: https://fastapi.tiangolo.com/tutorial/dependencies/classes-as-dependencies/
