Dependencies API Reference
The Dependencies API in Nexios provides a powerful dependency injection system that allows you to manage and inject dependencies into your route handlers. This system helps in creating more maintainable, testable, and modular code.
Basic Usage
The Depend
class is used to declare dependencies in route handlers.
from nexios import Depend
from nexios.http import Request, Response
async def get_current_user(request: Request) -> User:
# Implementation to get current user
pass
@app.get("/profile")
async def profile(
request: Request,
response: Response,
user: User = Depend(get_current_user)
):
return response.json({"user": user.dict()})
Dependency Types
Function Dependencies
The most common type of dependency is a function that returns a value.
async def get_db():
db = Database()
await db.connect()
return db
@app.get("/items")
async def list_items(
request: Request,
response: Response,
db = Depend(get_db)
):
items = await db.query("SELECT * FROM items")
return response.json({"items": items})
Class Dependencies
You can also use classes as dependencies.
class Database:
def __init__(self):
self.connection = None
async def connect(self):
self.connection = await create_connection()
async def query(self, query: str):
return await self.connection.execute(query)
@app.get("/items")
async def list_items(
request: Request,
response: Response,
db: Database = Depend(Database)
):
items = await db.query("SELECT * FROM items")
return response.json({"items": items})
Parameter Dependencies
Dependencies can depend on other dependencies.
async def get_db():
return Database()
async def get_user_service(db = Depend(get_db)):
return UserService(db)
@app.get("/users")
async def list_users(
request: Request,
response: Response,
user_service = Depend(get_user_service)
):
users = await user_service.get_all()
return response.json({"users": users})
Dependency Scopes
Request Scope
Dependencies can be scoped to the request lifecycle.
async def get_request_id():
return str(uuid.uuid4())
@app.get("/items")
async def list_items(
request: Request,
response: Response,
request_id: str = Depend(get_request_id)
):
return response.json({"request_id": request_id})
Application Scope
Dependencies can be shared across requests.
class Config:
def __init__(self):
self.settings = load_settings()
config = Config()
@app.get("/settings")
async def get_settings(
request: Request,
response: Response,
settings: Config = Depend(lambda: config)
):
return response.json({"settings": settings.settings})
Context-Aware Dependency Injection
Nexios provides a powerful, context-aware dependency injection system that allows you to access request-scoped data (and more) anywhere in your dependency tree, even in deeply nested dependencies.
What is Context?
The Context
object in Nexios is a special class that carries information about the current request and its environment. It is automatically created for each incoming request and is available throughout the entire dependency resolution process.
By default, the Context
includes:
request
: The currentRequest
objectuser
: The authenticated user (if available)
You can extend the Context
class to include more fields as needed for your application.
How to Use Context in Handlers and Dependencies
1. Type Annotation (Classic)
You can declare a context: Context = None
parameter in your handler or dependency. Nexios will automatically inject the current context:
from nexios.dependencies import Context
@app.get("/context-demo")
async def context_demo(req: Request, res: Response, context: Context = None):
return {"path": context.request.url.path, "trace_id": context.trace_id}
2. Default Value (No Type Annotation Needed)
You can also use context=Context()
as a parameter. Nexios will recognize this and inject the current context automatically:
@app.get("/auto-context")
async def auto_context_demo(req: Request, res: Response, context=Context()):
return {"path": context.request.url.path}
This works for both handlers and dependencies, and even for deeply nested dependencies:
async def get_user(context=Context()):
# context is injected automatically
return {"user": "alice", "path": context.request.url.path}
@app.get("/user-path")
async def user_path(req: Request, res: Response, user=Depend(get_user)):
return user
3. Accessing Context Anywhere
If you need to access the context outside of a function parameter, you can use the current_context
variable:
from nexios.dependencies import current_context
def some_function():
ctx = current_context.get()
print(ctx.request.url.path)
Advanced: Customizing Context
You can subclass or extend the Context
class to add more fields or methods relevant to your application:
class MyContext(Context):
def __init__(self, request=None, user=None, my_custom_field=None, **kwargs):
super().__init__(request=request, user=user, **kwargs)
self.my_custom_field = my_custom_field
Then, you can configure Nexios to use your custom context class if needed.
Why Use Context?
- Consistency: All dependencies and handlers can access the same request-scoped data.
- Flexibility: Add custom fields to the context for your app's needs.
- No Boilerplate: No need to manually pass context through every function.
- Async-Safe: Works seamlessly with async code and deeply nested dependencies.
Example: Deeply Nested Context
async def dep_a(context=Context()):
return f"A: {context.request.url.path}"
async def dep_b(a=Depend(dep_a), context=Context()):
return f"B: {a}, {context.request.url.path}"
@app.get("/deep-context")
async def deep_context(req: Request, res: Response, b=Depend(dep_b)):
return {"result": b}
In this example, both dep_a
and dep_b
receive the same context object, even though they are nested.
Dependency Validation
Type Validation
Dependencies can validate their return types.
from typing import Optional
from pydantic import BaseModel
class User(BaseModel):
id: int
username: str
email: str
async def get_current_user() -> User:
# Implementation
pass
@app.get("/profile")
async def profile(
request: Request,
response: Response,
user: User = Depend(get_current_user)
):
return response.json({"user": user.dict()})
Error Handling
Dependencies can handle errors gracefully.
async def get_current_user():
try:
# Implementation
return user
except Exception as e:
raise HTTPException(401, "Invalid credentials")
@app.get("/profile")
async def profile(
request: Request,
response: Response,
user = Depend(get_current_user)
):
return response.json({"user": user.dict()})
Advanced Usage
Caching Dependencies
Dependencies can be cached for better performance.
from functools import lru_cache
@lru_cache()
async def get_cached_data():
# Expensive operation
return data
@app.get("/data")
async def get_data(
request: Request,
response: Response,
data = Depend(get_cached_data)
):
return response.json({"data": data})
Async Dependencies
Dependencies can be asynchronous.
async def get_async_data():
async with aiohttp.ClientSession() as session:
async with session.get("https://api.example.com/data") as response:
return await response.json()
@app.get("/external-data")
async def get_external_data(
request: Request,
response: Response,
data = Depend(get_async_data)
):
return response.json({"data": data})
Dependency Overrides
Dependencies can be overridden for testing.
async def get_test_db():
return TestDatabase()
# In tests
app.dependency_overrides[get_db] = get_test_db
Generator and Async Generator Dependencies
Nexios supports both synchronous and asynchronous generator dependencies for resource management. Use yield
in your dependency to provide a resource and run cleanup code after the request:
# Synchronous generator dependency
def get_resource():
resource = acquire()
try:
yield resource
finally:
release(resource)
# Async generator dependency
async def get_async_resource():
resource = await acquire_async()
try:
yield resource
finally:
await release_async(resource)
@app.get("/resource")
async def use_resource(request, response, r=Depend(get_resource)):
...
@app.get("/async-resource")
async def use_async_resource(request, response, r=Depend(get_async_resource)):
...
- Cleanup code in the
finally
block is always executed after the request, even if an exception occurs. - Both sync and async generator dependencies are supported.
Best Practices
Keep Dependencies Focused: Each dependency should have a single responsibility.
Use Type Hints: Always use type hints for better code clarity and IDE support.
Handle Errors: Properly handle and propagate errors in dependencies.
Document Dependencies: Document the purpose and requirements of each dependency.
Test Dependencies: Write unit tests for your dependencies.
async def test_get_current_user():
user = await get_current_user()
assert isinstance(user, User)
assert user.id is not None
- Use Dependency Injection: Use dependency injection to make your code more testable.
class UserService:
def __init__(self, db: Database):
self.db = db
async def get_user(self, user_id: int) -> User:
return await self.db.query(f"SELECT * FROM users WHERE id = {user_id}")
# In route
@app.get("/users/{user_id}")
async def get_user(
request: Request,
response: Response,
user_id: int,
user_service: UserService = Depend(lambda: UserService(get_db()))
):
user = await user_service.get_user(user_id)
return response.json({"user": user.dict()})
- Use Pydantic Models: Use Pydantic models for dependency validation.
from pydantic import BaseModel
class UserCreate(BaseModel):
username: str
email: str
password: str
@app.post("/users")
async def create_user(
request: Request,
response: Response,
user_data: UserCreate = Depend(lambda: UserCreate(**request.json()))
):
user = await create_user_in_db(user_data)
return response.status(201).json({"user": user.dict()})
App-level and Router-level Dependencies
You can apply dependencies to all routes in the app or in a router by passing a dependencies
argument:
- App-level:
NexiosApp(dependencies=[...])
applies to every route in the app. - Router-level:
Router(dependencies=[...])
applies to every route in that router.
Example: App-level
from nexios import NexiosApp, Depend
def global_dep():
return "global-value"
app = NexiosApp(dependencies=[Depend(global_dep)])
Example: Router-level
from nexios import Router, Depend
def router_dep():
return "router-value"
router = Router(prefix="/api", dependencies=[Depend(router_dep)])
These dependencies are resolved before any route-specific dependencies.