Middleware
Middleware in Nexios is a powerful feature that allows you to intercept, process, and modify requests and responses as they flow through your application. It acts as a pipeline, enabling you to implement cross-cutting concerns such as logging, authentication, validation, and response modification in a modular and reusable way. This documentation provides a comprehensive guide to understanding and using middleware in Nexios.
Middleware Fundamentals
Middleware in Nexios provides:
- Request/Response Pipeline: Process requests before and after handlers
- Cross-cutting Concerns: Implement logging, auth, CORS, etc. once
- Modular Design: Each middleware has a single responsibility
- Reusability: Middleware can be shared across different routes
- Order Control: Middleware executes in the order they're added
- Error Handling: Middleware can catch and handle exceptions
Middleware Best Practices
- Single Responsibility: Each middleware should do one thing well
- Order Matters: Add middleware in logical order (auth before business logic)
- Error Handling: Always handle exceptions gracefully
- Performance: Keep middleware lightweight and efficient
- Reusability: Design middleware to be reusable across projects
- Documentation: Document what each middleware does and its requirements
- Testing: Test middleware in isolation and with your application
Common Middleware Patterns
- Authentication: Verify user identity and permissions
- Logging: Record request/response information
- CORS: Handle cross-origin requests
- Rate Limiting: Prevent abuse and ensure fair usage
- Compression: Reduce response size for better performance
- Security Headers: Add security-related HTTP headers
- Request Validation: Validate and sanitize request data
- Response Transformation: Modify responses before sending
How Middleware Works
Middleware functions are executed in a sequence, forming a pipeline that processes incoming requests and outgoing responses. Each middleware function has access to the request (req
), response (res
), and a next
function to pass control to the next middleware or the final route handler.
Key Responsibilities of Middleware
- Modify the Request – Add headers, parse data, or inject additional context.
- Block or Allow Access – Enforce authentication, rate limiting, or other access controls.
- Modify the Response – Format responses, add headers, or compress data.
- Pass Control – Call
next()
to continue processing the request or terminate early.
Middleware Flow
The middleware pipeline follows this pattern:
- Pre-processing: Execute code before the handler
- Call next(): Pass control to the next middleware or handler
- Post-processing: Execute code after the handler returns
- Return response: Send the final response back to the client
Basic Middleware Example
Below is a simple example demonstrating how to define and use middleware in a Nexios application:
from nexios import NexiosApp
from datetime import datetime
app = NexiosApp()
# Middleware 1: Logging
async def my_logger(req, res, next):
print(f"Received request: {req.method} {req.path}")
await next() # Pass control to the next middleware or handler
# If you forget to call await next(), the request will hang or time out and the client will not receive a response.
# Middleware 2: Request Timing
async def request_time(req, res, next):
req.request_time = datetime.now() # Store request time in context
await next()
# Middleware 3: Cookie Validation
async def validate_cookies(req, res, next):
if "session_id" not in req.cookies:
return res.json({"error": "Missing session_id cookie"}, status_code=400)
await next()
# If you return a response before calling next(), the pipeline is short-circuited and no further middleware or handlers will run. This is useful for early exits, such as authentication failures.
# Add middleware to the application
app.add_middleware(my_logger)
app.add_middleware(request_time)
app.add_middleware(validate_cookies)
# Route Handler
@app.get("/")
async def hello_world(req, res):
return res.text("Hello, World!")
::: tip 💡Tip
All code before `await next()` is executed before the route handler.
:::
## **Order of Execution**
Middleware functions are executed in the order they are added. The flow of execution is as follows:
1. **Pre-Processing** – Middleware functions execute before the route handler.
2. **Route Handler** – The request is processed by the route handler.
3. **Post-Processing** – Middleware functions execute after the route handler.
Incoming request └──> Middleware 1 (logs) └──> Middleware 2 (auth check) └──> Route handler (e.g., /profile) └──> Response is built ←──── Middleware 2 resumes (e.g., modify response) ←──── Middleware 1 resumes ←──── Final response sent
::: tip 💡Tip
Middleware functions are executed in the order they are added. Ensure that middleware with dependencies (e.g., authentication before authorization) is added in the correct sequence.
:::
***
## What is `cnext`?
In Nexios, middleware functions rely on a continuation callback (commonly called next, cnext, or callnext) to pass control to the next stage of the request pipeline. This parameter is crucial for request flow but its name is completely flexible — you're free to call it whatever makes sense for your codebase.
## **Class-Based Middleware**
Nexios supports class-based middleware for better organization and reusability. A class-based middleware must inherit from `BaseMiddleware` and implement the following methods:
* **`process_request(req, res, cnext)`** – Executed before the request reaches the handler.
* **`process_response(req, res)`** – Executed after the handler has processed the request.
### **Example: Class-Based Middleware**
```python
from nexios.middleware import BaseMiddleware
class ExampleMiddleware(BaseMiddleware):
async def process_request(self, req, res, cnext):
"""Executed before the request handler."""
print("Processing Request:", req.method, req.url)
await cnext(req, res) # Pass control to the next middleware or handler
# If you use the wrong parameter order in your methods, Nexios will raise an error at startup.
# If you forget to call await cnext(req, res), the request will not reach the handler and the client will not receive a response.
async def process_response(self, req, res):
"""Executed after the request handler."""
print("Processing Response:", res.status_code)
return res # Must return the modified response
# If you forget to return the response in process_response, the client will not receive the intended response.
Method Breakdown
process_request(req, res, cnext)
- Used for pre-processing tasks like logging, authentication, or data injection.
- Must call
await cnext(req, res)
to continue processing.
process_response(req, res)
- Used for post-processing tasks like modifying the response or logging.
- Must return the modified
res
object.
If you forget to return the response in process_response, the client will not receive the intended response.
If you use the wrong parameter order in your methods, Nexios will raise an error at startup.
You can handle errors in middleware by wrapping logic in try/except and returning a custom error response.
from nexios.middleware import BaseMiddleware
class ErrorCatchingMiddleware(BaseMiddleware):
async def process_request(self, req, res, cnext):
try:
await cnext(req, res)
except Exception as exc:
return res.json({"error": str(exc)}, status_code=500)
# If you raise an error in middleware and do not handle it, Nexios will return a 500 error. Always use try/except for critical middleware logic.
async def process_response(self, req, res):
return res
Route-Specific Middleware
Route-specific middleware applies only to a particular route. This is useful for applying middleware logic to specific endpoints without affecting the entire application.
Example: Route-Specific Middleware
async def auth_middleware(req, res, cnext):
if not req.headers.get("Authorization"):
return res.json({"error": "Unauthorized"}, status_code=401)
await cnext(req, res)
# If you forget to call await cnext(req, res) in route-specific middleware, the request will not reach the handler and the client will not receive a response.
@app.route("/profile", "GET", middleware=[auth_middleware])
async def get_profile(req, res):
return res.json({"message": "Welcome to your profile!"})
⚙️ Execution Order:auth_middleware → get_profile handler → response sent
If you forget to call await cnext(req, res) in route-specific middleware, the request will not reach the handler.
Router-Specific Middleware
Router-specific middleware applies to all routes under a specific router. This is useful for grouping middleware logic for a set of related routes.
Example: Router-Specific Middleware
admin_router = Router()
async def admin_auth(req, res, cnext):
if not req.headers.get("Admin-Token"):
return res.json({"error": "Forbidden"}, status_code=403)
await cnext(req, res)
# Returning a response before calling cnext will stop further processing for this request.
admin_router.add_middleware(admin_auth) # Applies to all routes inside admin_router
@admin_router.route("/dashboard", "GET")
async def dashboard(req, res):
return res.json({"message": "Welcome to the admin dashboard!"})
app.mount_router("/admin", admin_router) # Mount router at "/admin"
Execution Order:admin_auth → dashboard handler → response sent
Nexios Middleware vs. ASGI Middleware
Nexios offers two ways to add middleware to your application: app.add_middleware()
and app.wrap_asgi()
. While both are used to hook into the request/response lifecycle, they serve different purposes and are designed for different types of middleware.
add_middleware
: For Nexios-Specific Middleware
The app.add_middleware()
method is designed for middleware that is tightly integrated with the Nexios framework. This type of middleware operates on Nexios's Request
and Response
objects, giving you access to the rich features and abstractions that Nexios provides.
When to use add_middleware
:
- You want to interact with Nexios-specific objects like
Request
andResponse
. - Your middleware needs to access path parameters, parsed query parameters, or the request body in a convenient way.
- You want to take advantage of Nexios's dependency injection system within your middleware.
Example:
from nexios import NexiosApp, Request, Response
app = NexiosApp()
async def nexios_style_middleware(req: Request, res: Response, next_call):
# This middleware has access to the Nexios Request and Response objects
print(f"Request path: {req.path}")
print(f"Query params: {req.query_params}")
await next_call()
res.set_header("X-Nexios-Middleware", "true")
app.add_middleware(nexios_style_middleware)
@app.get("/")
async def home(req: Request, res: Response):
res.send("Hello from Nexios!")
wrap_asgi
: For Standard ASGI Middleware
The app.wrap_asgi()
method is used to add standard ASGI middleware to your application. ASGI middleware is a lower-level type of middleware that conforms to the ASGI specification. It operates directly on the raw ASGI scope
, receive
, and send
callables.
This is particularly useful when you want to use third-party ASGI middleware that is not specific to Nexios.
When to use wrap_asgi
:
- You need to integrate a third-party ASGI middleware (e.g., from a library like
asgi-correlation-id
). - Your middleware needs to operate at a lower level, before the request is processed by Nexios's routing and request/response handling.
- The middleware is designed to be framework-agnostic.
Example (using a hypothetical third-party ASGI middleware):
Let's say you have a third-party library that provides a GZip middleware for ASGI applications.
from nexios import NexiosApp
from some_asgi_library import GZipMiddleware # A hypothetical third-party middleware
app = NexiosApp()
# Wrap the Nexios application with the third-party ASGI middleware
app.wrap_asgi(GZipMiddleware, minimum_size=1000)
@app.get("/")
async def home(req, res):
# The response will be gzipped by the middleware if it's large enough
res.send("This is a long string that will hopefully be compressed." * 100)
When to Use Which?
Feature | add_middleware | wrap_asgi |
---|---|---|
Abstraction Level | High-level (Nexios Request /Response ) | Low-level (ASGI scope , receive , send ) |
Framework Specific | Nexios-specific | Framework-agnostic (standard ASGI) |
Use Case | Business logic, auth, interacting with Nexios features | Third-party middleware, low-level request manipulation |
Example | Custom logging, modifying Nexios Response | GZip compression, CORS handling from a standard library |
By understanding the difference between these two methods, you can choose the right tool for the job and build more powerful and flexible applications with Nexios.
Using @use_for_route
Decorator
The @use_for_route
decorator binds a middleware function to specific routes or route patterns, ensuring that the middleware only executes when a matching route is accessed.
Example: @use_for_route
Decorator
from nexios.middleware.utils import use_for_route
@use_for_route("/dashboard")
async def log_middleware(req, res, cnext):
print(f"User accessed {req.path.url}")
await cnext(req, res) # Proceed to the next function (handler or middleware)
Always call await next()
in middleware to ensure the request continues processing. Failing to do so will block the request pipeline.
⚠️ Warning
Avoind modifying the request object in middleware. This can lead to unexpected behavior or security issues.
⚠️ Warning
Modifying the response object should be done after the request is processed. It's best to use the process_response
method of middleware or callnext
Raw ASGI Middleware
Nexios allows you to use raw ASGI middleware for scenarios where you need lower-level control over the ASGI protocol. This is especially useful when integrating with third-party ASGI middleware, performing operations that require direct access to the ASGI scope
, or when you need to manipulate the request/response cycle before Nexios processes the request.
Raw ASGI middleware operates directly on the scope
, receive
, and send
callables, and is completely framework-agnostic. Unlike Nexios middleware, it does not have access to Nexios-specific abstractions like Request
or Response
objects.
Function-Based Raw Middleware
A function-based raw middleware wraps the ASGI app and must call the next app in the chain. If you forget to call await app(scope, receive, send)
, the request will never reach the application and the client will hang.
def raw_middleware(app):
async def middleware(scope, receive, send):
# You can inspect or modify the scope here
# For example, add a custom key:
scope["custom_key"] = "custom_value"
# Always call the next app in the chain
await app(scope, receive, send)
return middleware
app.wrap_asgi(raw_middleware)
If you modify the scope
, be careful not to overwrite required ASGI keys or introduce security issues. Always validate any changes you make.
Class-Based Raw Middleware
A class-based raw middleware provides more flexibility and can maintain state between requests if needed. The class must implement the __call__
method and accept the ASGI app as its first argument.
class RawMiddleware:
def __init__(self, app, *args, **kwargs):
self.app = app
# You can store args/kwargs for configuration
async def __call__(self, scope, receive, send):
# Perform actions before the request is processed
# For example, log the incoming scope
print(f"ASGI scope: {scope}")
try:
await self.app(scope, receive, send)
except Exception as exc:
# Handle errors gracefully
print(f"Error in raw middleware: {exc}")
raise
# You can also perform actions after the response is sent
app.wrap_asgi(RawMiddleware, some_option=True)
If you raise an error in raw middleware and do not handle it, the client will receive a 500 error. Always use try/except for critical logic.
When to Use Raw Middleware
- Integrating third-party ASGI middleware (e.g., CORS, GZip, Sentry, etc.)
- Performing low-level request/response manipulation before Nexios processes the request
- Adding features that require direct access to the ASGI protocol
- Ensuring compatibility with other ASGI frameworks or tools
Common Mistakes
- Not calling
await app(scope, receive, send)
: The request will hang and the client will not receive a response. - Modifying the
scope
incorrectly: Can break downstream middleware or the application. - Not handling exceptions: Unhandled errors will result in a 500 error for the client.
- Assuming access to Nexios-specific objects: Raw middleware only works with ASGI primitives.
By understanding and using raw ASGI middleware appropriately, you can extend your Nexios application with powerful, low-level features and integrate seamlessly with the broader ASGI ecosystem.