Skip to content

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

  1. Single Responsibility: Each middleware should do one thing well
  2. Order Matters: Add middleware in logical order (auth before business logic)
  3. Error Handling: Always handle exceptions gracefully
  4. Performance: Keep middleware lightweight and efficient
  5. Reusability: Design middleware to be reusable across projects
  6. Documentation: Document what each middleware does and its requirements
  7. 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:

  1. Pre-processing: Execute code before the handler
  2. Call next(): Pass control to the next middleware or handler
  3. Post-processing: Execute code after the handler returns
  4. 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:

python
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

  1. 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.
  2. 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.

python
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

python
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

python
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 and Response.
  • 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:

python
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.

python
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?

Featureadd_middlewarewrap_asgi
Abstraction LevelHigh-level (Nexios Request/Response)Low-level (ASGI scope, receive, send)
Framework SpecificNexios-specificFramework-agnostic (standard ASGI)
Use CaseBusiness logic, auth, interacting with Nexios featuresThird-party middleware, low-level request manipulation
ExampleCustom logging, modifying Nexios ResponseGZip 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

python
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.

python
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.

python
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.