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:
req.user.organizationId(authenticated user)req.query.organizationIdx-tenant-slugheader,req.query.slug, orreq.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.