Node / Express Guide

Middleware

Tenant resolution, auth, role guards, and app-level middleware.

Tenant Middleware

Resolves organizationId from the request context and attaches it to req.organizationId. Checked in priority order:

  1. req.user.organizationId (authenticated user)
  2. req.query.organizationId
  3. x-tenant-slug header, req.query.slug, or req.params.slug → DB lookup

The middleware also uses a WeakMap<Request> cache to avoid redundant DB lookups within the same request.

// src/middleware/tenant.middleware.ts
import { Request, Response, NextFunction } from "express";
import { UnprocessableEntityError } from "@codisolutions23/node-utils";
import { useOrganizationRepo } from "../repositories/organization.repo";

export interface TenantRequest extends Request {
  organizationId?: string;
  slug?: string;
  user?: { organizationId?: string; [key: string]: any };
}

const requestCache = new WeakMap<Request, { organizationId: string }>();

export function resolveTenant(required: boolean = true) {
  return async (req: Request, res: Response, next: NextFunction) => {
    const tenantReq = req as TenantRequest;

    // Check WeakMap cache first
    const cached = requestCache.get(req);
    if (cached?.organizationId) {
      tenantReq.organizationId = cached.organizationId;
      return next();
    }

    let organizationId = tenantReq.user?.organizationId;

    if (!organizationId && req.query.organizationId) {
      organizationId = req.query.organizationId as string;
    }

    if (!organizationId) {
      const tenantSlug =
        req.headers["x-tenant-slug"] || req.query.slug || req.params.slug;
      if (tenantSlug && typeof tenantSlug === "string") {
        const { getOrganizationBySlug, getPublicOrganizationBySlug } =
          useOrganizationRepo();
        const org =
          (await getOrganizationBySlug(tenantSlug)) ||
          (await getPublicOrganizationBySlug(tenantSlug));
        if (org?._id) {
          organizationId = org._id.toString();
          tenantReq.slug = tenantSlug;
        }
      }
    }

    if (!organizationId && required) {
      return next(
        new UnprocessableEntityError(
          "Organization could not be determined. Please ensure you access via a valid subdomain.",
        ),
      );
    }

    if (organizationId) {
      tenantReq.organizationId = organizationId;
      requestCache.set(req, { organizationId });
    }

    next();
  };
}

export const resolveOptionalTenant = resolveTenant(false);
export const resolveRequiredTenant = resolveTenant(true);
export default resolveOptionalTenant; // default export for global use

Usage on routes:

// Optional — continues if org cannot be determined
router.post("/login", resolveOptionalTenant, login);

// Required — throws 422 if org cannot be determined
router.get("/", resolveRequiredTenant, getItems);
The tenant middleware runs globally in app.ts (as default import resolveOptionalTenant), so most routes automatically have req.organizationId populated. Use resolveRequiredTenant per-route when the org context is mandatory.

Auth Middleware

Validates the Authorization: Bearer <token> header and checks the JWT is not revoked:

import { authenticate } from "@codisolutions23/node-utils";
import { useRevokedTokenRepo } from "../repositories/revoked-token.repo";

const { existsByJti } = useRevokedTokenRepo();
const accessTokenSecret = process.env.ACCESS_TOKEN_SECRET || "";
const authMiddleware = authenticate(accessTokenSecret, existsByJti);

router.get("/profile", authMiddleware, getProfile);

On success, attaches req.user (decoded JWT payload with .id, .role, .organizationId, .jti) and req.token (raw token string) to the request.

Role Guard Middleware

Role guards perform their own JWT verification — they don't wrap authenticate(). Each guard verifies the token, checks jti revocation, then asserts the user's role:

// src/middleware/role-guard.middleware.ts
import {
  AuthenticatedRequest,
  ForbiddenError,
  UnauthorizedError,
} from "@codisolutions23/node-utils";
import jwt from "jsonwebtoken";
import { UserType } from "../enums/user.enum";

export function requireRoles(
  allowedRoles: UserType[],
  accessTokenSecret: string,
  existsByJti: (jti: string) => Promise<boolean>,
) {
  return async (
    req: AuthenticatedRequest,
    res: Response,
    next: NextFunction,
  ) => {
    const authHeader = req.headers.authorization;
    if (!authHeader?.startsWith("Bearer "))
      throw new UnauthorizedError("Access token is required.");

    const token = authHeader.split(" ")[1];
    const decoded = jwt.verify(token, accessTokenSecret) as jwt.JwtPayload;

    if (!decoded?.role) throw new UnauthorizedError("Invalid token.");

    if (decoded.jti) {
      const isRevoked = await existsByJti(decoded.jti);
      if (isRevoked) throw new UnauthorizedError("Token revoked.");
    }

    if (!allowedRoles.includes(decoded.role as UserType))
      throw new ForbiddenError("Insufficient permissions.");

    req.user = { ...decoded, id: decoded.user || decoded.id || "" };
    req.token = token;
    next();
  };
}

Pre-composed role guards:

// requireAdmin     → ADMIN only
export const requireAdmin = (secret, existsByJti) =>
  requireRoles([UserType.ADMIN], secret, existsByJti);

// requireOwner     → OWNER only (not admin!)
export const requireOwner = (secret, existsByJti) =>
  requireRoles([UserType.OWNER], secret, existsByJti);

// requireAdminOrOwner → ADMIN or OWNER
export const requireAdminOrOwner = (secret, existsByJti) =>
  requireRoles([UserType.ADMIN, UserType.OWNER], secret, existsByJti);

// requireBranchManagerOrAbove → ADMIN, OWNER, or BRANCH_MANAGER
export const requireBranchManagerOrAbove = (secret, existsByJti) =>
  requireRoles(
    [UserType.ADMIN, UserType.OWNER, UserType.BRANCH_MANAGER],
    secret,
    existsByJti,
  );
requireOwner is OWNER-only — it does NOT include ADMIN. Use requireAdminOrOwner when admins should also have access.

Usage in a route file:

const accessTokenSecret = process.env.ACCESS_TOKEN_SECRET || "";
const adminMiddleware = requireAdmin(accessTokenSecret, existsByJti);
const branchManagerMiddleware = requireBranchManagerOrAbove(
  accessTokenSecret,
  existsByJti,
);

router.get("/", authMiddleware, getAll);
router.post("/", branchManagerMiddleware, create);
router.delete("/:id", adminMiddleware, remove);

App-Level Middleware Order

Register middleware in this order in src/app.ts:

app.set("trust proxy", 1);

app.use(cors({ origin: "*", credentials: true })); // CORS
app.use(express.json()); // JSON body parser
app.use(helmet()); // Security headers
app.disable("x-powered-by"); // Remove X-Powered-By header

// Rate limiting — production only, 1000 req/min
if (!isDev) {
  app.use(
    rateLimit({
      windowMs: 1 * 60 * 1000,
      max: 1000,
      standardHeaders: true,
      legacyHeaders: false,
    }),
  );
}

// Global middleware — runs before all routes
app.use(tenantMiddleware); // resolve organizationId
app.use(loadOrganizationMiddleware); // attach full org object

app.use("/api", router()); // Routes
app.use(errorHandler); // Error handler — must be last
No express.urlencoded() — this is a JSON-only API. Rate limiting is production-only at 1000 requests per minute. Tenant and load-organization middleware run globally, not per-route.