res.locals is a plain object scoped to a single request-response cycle — the standard way to pass data between middleware and route handlers without polluting req or global state. Discarded once the response is sent.
Set in middleware, read in handler
// middleware
res.locals.userID = 'user-123'
res.locals.accessibleSets = ['set-abc', 'set-def']
next()
// handler
const userID = res.locals.userID as stringTypeScript typing
res.locals is Record<string, any> by default. Use a typed interface + Partial<> so TypeScript reminds you the middleware might not have run:
export interface AuthorisationLocals {
accessibleSets: string[]
userID: string
}
const locals = res.locals as Partial<AuthorisationLocals>
const accessibleSets = locals.accessibleSets || []Helper getter pattern
Export a typed getter from the middleware file to avoid scattering casts:
// set-filter.ts
export const getAccessibleSets = (res: Response): string[] => {
const locals = res.locals as Partial<AuthorisationLocals>
return locals.accessibleSets || []
}
// dashboard.ts
const accessibleSets = getAccessibleSets(res)Middleware ordering matters
A handler can only read a value if the middleware setting it is registered earlier in the chain.
// ✅
router.get('/dashboard/sessions', AuthenticateBb3Token, SetFilterMiddleware, GetSessions)
// ❌ GetSessions runs before accessibleSets is set
router.get('/dashboard/sessions', GetSessions, SetFilterMiddleware)