Skip to content

Commit dd41a42

Browse files
authored
Global/Router decorators (aka middlewares) (#1525)
* add_decorator
1 parent 52d510a commit dd41a42

File tree

7 files changed

+963
-0
lines changed

7 files changed

+963
-0
lines changed

docs/docs/guides/decorators.md

Lines changed: 364 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,364 @@
1+
# Decorators
2+
3+
Django Ninja provides flexible decorator support to wrap your API operations with additional functionality like caching, logging, authentication checks, or any custom logic.
4+
5+
## Understanding Decorator Modes
6+
7+
Django Ninja supports two modes for applying decorators:
8+
9+
10+
### OPERATION Mode (Default)
11+
- Applied **after** Django Ninja's validation
12+
- Wraps the operation function with validated data
13+
- Has access to parsed and validated parameters
14+
- Useful for: business logic, logging with validated data, post-validation checks
15+
16+
### VIEW Mode
17+
- Applied **before** Django Ninja's validation
18+
- Wraps the entire Django view function
19+
- Has access to the raw Django request
20+
- Useful for: caching, rate limiting, Django middleware-like functionality
21+
- Similar to Django's standard view decorators
22+
23+
24+
## Using `@decorate_view`
25+
26+
The `@decorate_view` decorator allows you to apply Django view decorators to individual endpoints:
27+
28+
```python
29+
from django.views.decorators.cache import cache_page
30+
from ninja import NinjaAPI
31+
from ninja.decorators import decorate_view
32+
33+
api = NinjaAPI()
34+
35+
@api.get("/cached")
36+
@decorate_view(cache_page(60 * 15)) # Cache for 15 minutes
37+
def cached_endpoint(request):
38+
return {"data": "This response is cached"}
39+
```
40+
41+
You can apply multiple decorators:
42+
43+
```python
44+
from django.views.decorators.cache import cache_page
45+
from django.views.decorators.vary import vary_on_headers
46+
47+
@api.get("/multi")
48+
@decorate_view(cache_page(300), vary_on_headers("User-Agent"))
49+
def multi_decorated(request):
50+
return {"data": "Multiple decorators applied"}
51+
```
52+
53+
## Using `add_decorator`
54+
55+
The `add_decorator` method allows you to apply decorators to multiple endpoints at once.
56+
57+
### Router-Level Decorators
58+
59+
Apply decorators to all endpoints in a router:
60+
61+
```python
62+
from ninja import Router
63+
64+
router = Router()
65+
66+
# Add logging to all operations in this router
67+
def log_operation(func):
68+
def wrapper(request, *args, **kwargs):
69+
print(f"Calling {func.__name__}")
70+
result = func(request, *args, **kwargs)
71+
print(f"Result: {result}")
72+
return result
73+
return wrapper
74+
75+
router.add_decorator(log_operation) # OPERATION mode by default
76+
77+
@router.get("/users")
78+
def list_users(request):
79+
return {"users": ["Alice", "Bob"]}
80+
81+
@router.get("/users/{user_id}")
82+
def get_user(request, user_id: int):
83+
return {"user_id": user_id}
84+
```
85+
86+
### API-Level Decorators
87+
88+
Apply decorators to all endpoints in your entire API:
89+
90+
```python
91+
from ninja import NinjaAPI
92+
93+
api = NinjaAPI()
94+
95+
# Add CORS headers to all responses (VIEW mode)
96+
def cors_headers(func):
97+
def wrapper(request, *args, **kwargs):
98+
response = func(request, *args, **kwargs)
99+
response["Access-Control-Allow-Origin"] = "*"
100+
return response
101+
return wrapper
102+
103+
api.add_decorator(cors_headers, mode="view")
104+
105+
# Now all endpoints will have CORS headers
106+
@api.get("/data")
107+
def get_data(request):
108+
return {"data": "example"}
109+
```
110+
111+
## Practical Examples
112+
113+
### Example 1: Request Timing
114+
115+
```python
116+
import time
117+
from functools import wraps
118+
119+
def timing_decorator(func):
120+
@wraps(func)
121+
def wrapper(request, *args, **kwargs):
122+
start = time.time()
123+
result = func(request, *args, **kwargs)
124+
duration = time.time() - start
125+
if isinstance(result, dict):
126+
result["_timing"] = f"{duration:.3f}s"
127+
return result
128+
return wrapper
129+
130+
router = Router()
131+
router.add_decorator(timing_decorator)
132+
133+
@router.get("/slow")
134+
def slow_endpoint(request):
135+
time.sleep(1)
136+
return {"message": "done"}
137+
# Returns: {"message": "done", "_timing": "1.001s"}
138+
```
139+
140+
### Example 2: Authentication Check (OPERATION mode)
141+
142+
```python
143+
from functools import wraps
144+
145+
def require_feature_flag(flag_name):
146+
def decorator(func):
147+
@wraps(func)
148+
def wrapper(request, *args, **kwargs):
149+
if not request.user.has_feature(flag_name):
150+
return {"error": f"Feature {flag_name} not enabled"}
151+
return func(request, *args, **kwargs)
152+
return wrapper
153+
return decorator
154+
155+
router = Router()
156+
router.add_decorator(require_feature_flag("new_api"))
157+
158+
@router.get("/new-feature")
159+
def new_feature(request):
160+
return {"feature": "enabled"}
161+
```
162+
163+
### Example 3: Response Caching (VIEW mode)
164+
165+
```python
166+
from django.core.cache import cache
167+
from functools import wraps
168+
import hashlib
169+
170+
def cache_response(timeout=300):
171+
def decorator(func):
172+
@wraps(func)
173+
def wrapper(request, *args, **kwargs):
174+
# Create cache key from request
175+
cache_key = hashlib.md5(
176+
f"{request.path}{request.GET.urlencode()}".encode()
177+
).hexdigest()
178+
179+
# Try to get from cache
180+
cached = cache.get(cache_key)
181+
if cached:
182+
return cached
183+
184+
# Call the view
185+
response = func(request, *args, **kwargs)
186+
187+
# Cache the response
188+
cache.set(cache_key, response, timeout)
189+
return response
190+
return wrapper
191+
return decorator
192+
193+
router = Router()
194+
router.add_decorator(cache_response(600), mode="view")
195+
```
196+
197+
## Decorator Execution Order
198+
199+
When multiple decorators are applied, they execute in this order:
200+
201+
1. API-level decorators (outermost)
202+
2. Parent router decorators
203+
3. Child router decorators
204+
4. Individual endpoint decorators (innermost)
205+
206+
```python
207+
api = NinjaAPI()
208+
parent_router = Router()
209+
child_router = Router()
210+
211+
api.add_decorator(api_decorator)
212+
parent_router.add_decorator(parent_decorator)
213+
child_router.add_decorator(child_decorator)
214+
215+
@child_router.get("/test")
216+
@decorate_view(endpoint_decorator)
217+
def endpoint(request):
218+
return {"result": "ok"}
219+
220+
parent_router.add_router("/child", child_router)
221+
api.add_router("/parent", parent_router)
222+
223+
# Execution order:
224+
# 1. api_decorator
225+
# 2. parent_decorator
226+
# 3. child_decorator
227+
# 4. endpoint_decorator
228+
# 5. endpoint function
229+
```
230+
231+
## Async Support
232+
233+
Decorators work with both sync and async views. When you have mixed sync/async endpoints in the same router, you need to create universal decorators that handle both cases.
234+
235+
### Universal Decorators for Mixed Sync/Async Routers
236+
237+
When you have a router with both sync and async endpoints, use `asyncio.iscoroutinefunction()` to detect the function type:
238+
239+
```python
240+
import asyncio
241+
from functools import wraps
242+
243+
def universal_decorator(func):
244+
if asyncio.iscoroutinefunction(func):
245+
# Handle async functions
246+
@wraps(func)
247+
async def async_wrapper(request, *args, **kwargs):
248+
# Your async logic here
249+
result = await func(request, *args, **kwargs)
250+
if isinstance(result, dict):
251+
result["decorated"] = True
252+
result["type"] = "async"
253+
return result
254+
return async_wrapper
255+
else:
256+
# Handle sync functions
257+
@wraps(func)
258+
def sync_wrapper(request, *args, **kwargs):
259+
# Your sync logic here
260+
result = func(request, *args, **kwargs)
261+
if isinstance(result, dict):
262+
result["decorated"] = True
263+
result["type"] = "sync"
264+
return result
265+
return sync_wrapper
266+
267+
router = Router()
268+
router.add_decorator(universal_decorator)
269+
270+
@router.get("/async")
271+
async def async_endpoint(request):
272+
await asyncio.sleep(0.1)
273+
return {"endpoint": "async"}
274+
275+
@router.get("/sync")
276+
def sync_endpoint(request):
277+
return {"endpoint": "sync"}
278+
```
279+
280+
### Async-Only Decorators
281+
282+
For routers with only async endpoints, you can use async decorators directly:
283+
284+
```python
285+
def async_timing_decorator(func):
286+
@wraps(func)
287+
async def wrapper(request, *args, **kwargs):
288+
start = time.time()
289+
result = await func(request, *args, **kwargs)
290+
duration = time.time() - start
291+
if isinstance(result, dict):
292+
result["_timing"] = f"{duration:.3f}s"
293+
return result
294+
return wrapper
295+
296+
router = Router()
297+
router.add_decorator(async_timing_decorator)
298+
299+
@router.get("/async")
300+
async def async_endpoint(request):
301+
await asyncio.sleep(1)
302+
return {"message": "async done"}
303+
```
304+
305+
### Sync Decorators on Async Views
306+
307+
You can also use sync decorators on async views by handling coroutines:
308+
309+
```python
310+
def sync_decorator(func):
311+
@wraps(func)
312+
def wrapper(request, *args, **kwargs):
313+
result = func(request, *args, **kwargs)
314+
315+
if asyncio.iscoroutine(result):
316+
# Handle async functions
317+
async def async_wrapper():
318+
actual_result = await result
319+
if isinstance(actual_result, dict):
320+
actual_result["sync_decorated"] = True
321+
return actual_result
322+
return async_wrapper()
323+
else:
324+
# Handle sync functions
325+
if isinstance(result, dict):
326+
result["sync_decorated"] = True
327+
return result
328+
return wrapper
329+
```
330+
331+
## When to Use Each Mode
332+
333+
### Use VIEW Mode When:
334+
- You need access to the raw Django request
335+
- Implementing caching at the HTTP level
336+
- Adding/modifying HTTP headers
337+
- Implementing rate limiting
338+
- Working with Django middleware patterns
339+
340+
### Use OPERATION Mode When:
341+
- You need access to validated/parsed data
342+
- Implementing business logic decorators
343+
- Adding data to responses
344+
- Logging with type-safe parameters
345+
- Post-validation security checks
346+
347+
## Best Practices
348+
349+
1. **Use `functools.wraps`**: Always use `@wraps(func)` to preserve function metadata
350+
351+
2. **Handle mixed sync/async routers**: When your router has both sync and async endpoints, use `asyncio.iscoroutinefunction(func)` to create universal decorators
352+
353+
3. **Choose the right approach for async**:
354+
- **Universal decorators**: Best for mixed routers (detect with `iscoroutinefunction`)
355+
- **Async-only decorators**: Best for async-only routers (simpler, cleaner)
356+
- **Sync decorators with coroutine handling**: Useful for legacy decorators
357+
358+
4. **Be mindful of performance**: Decorators add overhead, especially in VIEW mode
359+
360+
5. **Document side effects**: Clearly document what your decorators modify
361+
362+
6. **Keep decorators focused**: Each decorator should have a single responsibility
363+
364+
7. **Test both sync and async**: When using universal decorators, test both sync and async endpoints

docs/mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ nav:
6868
- guides/response/pagination.md
6969
- guides/response/response-renderers.md
7070
- Splitting your API with Routers: guides/routers.md
71+
- guides/decorators.md
7172
- guides/authentication.md
7273
- guides/throttling.md
7374
- guides/testing.md

ninja/decorators.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
from functools import partial
22
from typing import Any, Callable, Tuple
33

4+
from typing_extensions import Literal
5+
46
from ninja.operation import Operation
57
from ninja.types import TCallable
68
from ninja.utils import contribute_operation_callback
79

10+
# Type for decorator modes
11+
DecoratorMode = Literal["operation", "view"]
12+
813
# Since @api.method decorator is applied to function
914
# that is not always returns a HttpResponse object
1015
# there is no way to apply some standard decorators form

0 commit comments

Comments
 (0)