Middleware System

Learn how to intercept requests and responses using JopiJS middlewares (Global vs Route-specific).

Overview

JopiJS provides a flexible middleware system allowing you to intercept and modify HTTP requests and responses. Middlewares are useful for logging, authentication, security headers, data transformation, and more.

There are two main types of middlewares:

  1. Standard Middleware: Executed before the request reaches your route handler. It receives the JopiRequest and can return a Response (to intercept) or null (to continue).
  2. Post-Middleware: Executed after the route handler has generated a response. It receives the JopiRequest and the resulting Response, allowing you to modify headers or body content before sending it to the client.

1. Global Middlewares

Global middlewares apply to the entire application (or a subset of routes matching a pattern). They are configured in your server initialization file (usually src/index.ts or src/mod_myMod/serverInit.ts).

Configuration

Use the configure_middlewares() method on the website builder.

src/index.ts
import { jopiApp } from "jopijs";

jopiApp.startApp(
    import.meta,
    website => {
        website.configure_middlewares()
            // 1. Add a global PRE-middleware (runs before handler)
            .add_middleware(
                "GET", // Apply only to GET requests (or undefined for all)
                async (req) => {
                    console.log(`Incoming request: ${req.req_url}`);
                    // Return specific response to intercept, or null/void to continue
                    return null; 
                },
                { priority: "High" } // Optional options
            )

            // 2. Add a global POST-middleware (runs after handler)
            .add_postMiddleware(
                undefined, // Apply to ALL methods
                async (req, res) => {
                    // Example: Add a custom header to every response
                    res.headers.set("X-Powered-By", "JopiJS");
                    return res;
                }
            );
    }
);

Route Selection (RouteSelector)

By default, a global middleware applies to ALL routes unless you provide filtering options. You can use the routeSelector option to strictly control where the middleware is executed.

Logic: When a routeSelector is provided, the middleware is disabled by default. The route must be explicitly authorized by include, fromPath, or test. The exclude list always has priority (Veto).

.add_middleware(undefined, myMiddleware, { 
    routeSelector: {
        // 1. Veto: Never run on these paths (overrides everything else)
        exclude: ["/api/private/secret"],

        // 2. Explicitly Include: Always run on these exact paths
        include: ["/public/special-page"],

        // 3. Path Pattern: Run on this path and all sub-paths
        fromPath: "/api/",

        // 4. Custom Test: Programmatic validation
        test: (path) => path.startsWith("/legacy") && !path.includes("old")
    }
});

Selection Rules (Order of Precedence)

  1. Exclude: If the path is in the exclude list, the middleware is SKIPPED.
  2. Include: If the path is in the include list, the middleware is EXECUTED.
  3. FromPath: If the path matches the fromPath pattern, the middleware is EXECUTED.
    • fromPath: "/" matches everything.
    • fromPath: "/admin" matches exact /admin and sub-paths /admin/....
  4. Test: If the test function returns true, the middleware is EXECUTED.
  5. Default: If none of the above allow it, the middleware is SKIPPED.

Backward Compatibility: The top-level fromPath option in MiddlewareOptions is still supported and works as a shortcut for routeSelector: { fromPath: ... }.

Execution Lifecycle

The execution order is critical when combining authentication, caching, and logic.

  1. Role Verification: Checks required roles first. If failed, returns 401.
  2. Pre-Middlewares: Executes global and route-specific middlewares.
    • Note: These run before the HTML cache check.
  3. HTML Cache Check:
    • Hit: Returns cached response immediately.
    • Miss: Proceeds to Route Handler.
  4. Route Handler: Generates the initial response.
  5. Post-Middlewares: Modifies the response (e.g. adding headers).
  6. Cache Write: The final response (after Post-Middlewares) is saved to the HTML cache.

2. Route-Specific Middlewares (config.ts)

For more granular control, you can define middlewares directly within a module's config.ts file. This is ideal for logic that is specific to a single route or page.

In a config.ts file, you have access to a config object (instance of JopiRouteConfig).

src/mod_myModule/@routes/my-page/config.ts
import { config } from "jopijs/routes";

// Add middleware to ALL methods for this route
config.onALL.add_middleware(async (req) => {
    // Check something specific for this page
    if (!req.query.has("valid")) {
        return new Response("Invalid request", { status: 400 });
    }
});

// Add middleware only for POST requests on this route
config.onPOST.add_middleware(async (req) => {
    console.log("Processing POST on my-page");
});

// Add a POST-middleware (modify response) for GET requests
config.onGET.add_postMiddleware(async (req, res) => {
    res.headers.set("Cache-Control", "no-store");
    return res;
});

Middleware Signature

Pre-Middleware (JopiMiddleware)

type JopiMiddleware = (req: JopiRequest) => Response | Promise<Response | null> | null;
  • Input: JopiRequest object containing all request details.
  • Output:
    • Response: Stops the chain and sends this response immediately.
    • null / void: Continues to the next middleware or the route handler.

Post-Middleware (JopiPostMiddleware)

type JopiPostMiddleware = (req: JopiRequest, res: Response) => Response | Promise<Response>;
  • Input:
    • req: The original request.
    • res: The response generated by the handler (or previous post-middleware).
  • Output: Must return a Response object (either the original res modified, or a new one).