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:
- Standard Middleware: Executed before the request reaches your route handler. It receives the
JopiRequestand can return aResponse(to intercept) ornull(to continue). - Post-Middleware: Executed after the route handler has generated a response. It receives the
JopiRequestand the resultingResponse, 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.
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
routeSelectoris provided, the middleware is disabled by default. The route must be explicitly authorized byinclude,fromPath, ortest. Theexcludelist 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)
- Exclude: If the path is in the
excludelist, the middleware is SKIPPED. - Include: If the path is in the
includelist, the middleware is EXECUTED. - FromPath: If the path matches the
fromPathpattern, the middleware is EXECUTED.fromPath: "/"matches everything.fromPath: "/admin"matches exact/adminand sub-paths/admin/....
- Test: If the
testfunction returnstrue, the middleware is EXECUTED. - Default: If none of the above allow it, the middleware is SKIPPED.
Backward Compatibility: The top-level
fromPathoption inMiddlewareOptionsis still supported and works as a shortcut forrouteSelector: { fromPath: ... }.
Execution Lifecycle
The execution order is critical when combining authentication, caching, and logic.
- Role Verification: Checks required roles first. If failed, returns 401.
- Pre-Middlewares: Executes global and route-specific middlewares.
- Note: These run before the HTML cache check.
- HTML Cache Check:
- Hit: Returns cached response immediately.
- Miss: Proceeds to Route Handler.
- Route Handler: Generates the initial response.
- Post-Middlewares: Modifies the response (e.g. adding headers).
- 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).
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:
JopiRequestobject 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
Responseobject (either the originalresmodified, or a new one).