JCC Express

The Basics

Middleware

Introduction

Middleware sits in the HTTP pipeline between the incoming request and your route handler. Each piece is a function with the Express shape (req, res, next) => void (or async). It can run logic, short-circuit with a response, or call next() to continue.

JCC Express MVC distinguishes two layers:

  • Global middleware — wired once on the Express app for every request (framework defaults plus your HTTP kernel’s middlewares array).
  • Route middleware — attached only to routes that reference it, using string aliases from the kernel or raw Express handlers.

For how a request walks the full stack end-to-end, see Request lifecycle (final-documentation/Architecture Concept/Request-Lifecycle.md).


HTTP kernel

Your application’s HTTP kernel lives at app/Http/kernel.ts. It is registered on the container as HttpKernel (via bootstrap/app.ts.withKernel(Kernel)).

The class must satisfy KernelInterface:

  • middlewaresRequestHandler[] appended to the global Express stack after the framework’s built-in setup (body parsers, session, CORS, logging, rate limiting, HttpRequest / HttpResponse, and the per-request container binding for request / response / next).
  • middlewareAliasesRecord<string, RequestHandler>: short names resolved when you pass strings to Route.middleware([...]).

Example shape:

TypeScript
import { csrf, methodSpoofing } from "jcc-express-mvc/Core";
import { inertia } from "jcc-express-mvc/Core/Inertia";
import {
  auth,
  guest,
  apiAuth,
  loginRateLimit,
  registerRateLimit,
} from "jcc-express-mvc";

export class Kernel {
  public middlewares = [
    csrf(),
    methodSpoofing(),
    inertia({ rootView: "welcome", ssr: true }),
  ];

  public middlewareAliases: Record<string, any> = {
    auth,
    guest,
    apiAuth,
    loginThrottle: loginRateLimit,
    registerThrottle: registerRateLimit,
  };
}

Order in middlewares is execution order. Put cross-cutting concerns here (CSRF, method spoofing, Inertia) when they should apply broadly; use aliases for guards and throttles you attach selectively on routes.


Framework global stack (before your kernel)

The framework always registers a fixed prefix of Express middleware before your kernel's middlewares. That includes JSON/urlencoded parsing, cookies, session, flash, file uploads, CORS, Morgan, the app's rate limit config, and the request/response wrappers that attach jccSession. For req.session, req.jccSession, flash, and session(), see Session.

After those run, another middleware binds request, response, and next on the container for each request (used by request(), response(), view(), inertia(), etc.).

The numbered list of default middleware is documented in Request lifecycle; you normally do not edit that class from an app—customize CORS and rate limiting through app/Config where applicable, and extend behavior via the kernel or route middleware.


Route middleware

Attaching middleware

Use Route.middleware(...) immediately before the verb you register. The argument may be:

  • A string — must exist on kernel.middlewareAliases.
  • An array — mix strings (resolved to aliases) and raw RequestHandler functions.
  • A single RequestHandler — used as-is.
TypeScript
import { Route } from "jcc-express-mvc/Core";

Route.middleware(["auth"]).get("/home", [HomeController, "index"]);

Route.middleware(["guest"]).get("/login", (req, res) => {
  return res.inertia("auth/login");
});

If an alias string is missing, RouteBuilder throws (for example auth middleware not found in alias middleware).

Groups

Inside Route.prefix(...).group(...) or Route.group(...), call Route.middleware([...]) before defining routes. Nested groups flatten parent group middleware first, then child group middleware, then any middleware chained right before Route.get / post / etc.

TypeScript
Route.middleware(["auth"])
  .prefix("admin")
  .group((Route) => {
    Route.get("/dashboard", [AdminController, "index"]);
    Route.middleware(["registerThrottle"]).post("/users", [
      AdminController,
      "store",
    ]);
  });

When the route is registered on Express, global stack → matched route → route middleware (in that order) → handler.


Writing custom middleware

A middleware is a RequestHandler: (req, res, next) => void. Call next() when the request should continue; pass next(err) to delegate to the error handler. For async work, either use async / await and catch errors, or wrap with the framework’s asyncHandler if you need consistent error propagation.

Register reusable app middleware as functions or small modules, then either:

  • Add them to kernel.middlewareAliases under a name and reference that name in routes, or
  • Pass the function directly into Route.middleware(myHandler) or an array alongside strings.

Example alias:

TypeScript
// app/Http/Middleware/EnsureJson.ts
import type { RequestHandler } from "express";

export const ensureJson: RequestHandler = (req, res, next) => {
  if (!req.is("application/json")) {
    return res.status(415).json({ message: "JSON expected" });
  }
  next();
};
TypeScript
// app/Http/kernel.ts
import { ensureJson } from "./Middleware/EnsureJson";

export class Kernel {
  // ...
  public middlewareAliases: Record<string, any> = {
    // ...
    ensureJson,
  };
}
TypeScript
Route.middleware(["ensureJson"]).post("/api/echo", handler);

Built-in and framework exports

Common pieces you import rather than reimplement:

  • csrf() / methodSpoofing() from jcc-express-mvc/Core — web forms and unsafe verbs. Full CSRF usage (@csrf, AJAX headers, options) is in CSRF protection.
  • auth, guest, apiAuth — from jcc-express-mvc (session/JWT-style behavior depends on your auth setup).
  • loginRateLimit / registerRateLimit — aliased in the sample kernel as loginThrottle / registerThrottle.
  • inertia(...) — root view and SSR options for Inertia responses.

Tune global behavior in kernel.middlewares; tune per-route access with aliases on specific routes or groups.


Inspecting middleware on routes

List registered routes and their middleware:

Bash
bun artisanNode route:list

Middleware runs before your controller or closure. Keep handlers small, call next() when appropriate, and register aliases for anything you reuse across many routes.