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 string

TypeScript 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)