skip to content

Should the per-request session read run before routing, in a layout, or in each route module, and how do you decide?

level: principalimportance: should knowfreq 45%

answer

  1. three places, three jobs
  2. layouts do not feed route modules
  3. the early step does not know the route
  4. authoritative check next to the data
  5. one helper, greppable dependencies

basics

~20 s

Each site buys something different: a pre-routing step is uniform but coarse and cannot hand data to the route, a layout renders shared chrome, and a per-route read is explicit and precise. Most real apps use all three deliberately.

solid answer

~50 s

Treat the three as different jobs rather than competing answers. A step before routing runs on every matching request, knows nothing about which route matched, may run in a constrained runtime, and — importantly — usually cannot pass its resolved session down into route modules as data. A layout is the right home for shared chrome that shows the user, but in most file-based routers a layout has no channel to hand data to the route rendering inside it, and it may not re-run on every navigation. A per-route read is explicit: the route states its own dependency, and per-request deduplication keeps the repeats free. My rule is that the authoritative read lives next to the thing it protects — the route or the write — while the early step handles cheap, uniform concerns, and the convention is one shared helper so every dependency on identity is greppable.

go deeper

for a junior

Know that the session can be read in more than one place, and that a read in a layout does not automatically give the page inside it the same data.

for a middle

Compare the three placements on what each can and cannot do, especially the missing data channel from a layout to the route module and the coarse, route-blind nature of a step that runs before routing.

for a senior

Argue placement from failure modes you have seen: a check skipped by a client-side navigation, a layout that did not re-run, an early step that became the latency floor for every anonymous request.

for a principal

Own the convention rather than the individual call: one read helper, authoritative checks beside the data and writes, cheap uniform bounces early, and a deliberately small personalised surface.

## Three places, three different jobs | Place | When it runs | Good at | Cannot do | |---|---|---|---| | Before routing | Every request matching its pattern, before a route is chosen | One uniform bounce; cheap coarse decisions; owning the response, so it may set cookies | Know which route matched or what it will show; hand its result to route modules as ordinary data in most frameworks | | In a layout | When that layout renders, which may be less often than every navigation | Rendering shared chrome that names the user | Pass data down to the route module beneath it in most file-based routers | | In the route module | Every time that route is produced | Precision: this route's own dependency, stated locally | Give you one place to change a rule across a hundred routes | The column that decides most arguments is the last one. Teams reach for a single read "at the top" expecting it to feed everything below, and discover that route modules are siblings in data terms, not children. ## Why a layout cannot be the single read A layout wraps a subtree visually and, in most routers, is fetched and rendered as a peer of the route rather than as its data provider. Two consequences: - The route module does not receive the layout's data, so it reads again — fine, because deduplication makes that one lookup, but it means the layout was never the single source. - Layouts often persist across navigations between their children, so a read placed there can be **staler** than the route below it. Anything security-relevant placed only in a layout can therefore be skipped entirely on a client-side navigation. Use a layout for chrome. Do not use it as the place a rule is enforced. ## Why the pre-routing step cannot be the single read either It has real strengths: it is uniform, it runs before any rendering work is spent, and it owns the response so it can redirect or set cookies. Its limits are just as real: - It frequently runs in a **constrained runtime** with a smaller API surface and stricter latency expectations, so a heavy session-store lookup there taxes every request including ones that never needed it. - It matches on URL shape, so its decisions are coarse; the interesting question is usually about the specific record a route is going to show. - Its result reaches the render only if you deliberately pass it — via a request-scoped value, or by attaching something to the request — which couples the two and is easy to get wrong. ## How I decide 1. **Correctness first.** The check that must not be bypassed goes next to the thing it protects: the route that renders the data, or the handler that performs the write. Anything earlier is defence in depth, not the control. 2. **Then uniformity.** If every route in a section needs the same bounce, put that bounce in the early step so a new route in the section is protected on the day it is added rather than the day someone remembers. 3. **Then cost.** Keep the early step cheap. If resolving a session is expensive, do the cheap structural check early and the authoritative one where it matters. 4. **Then ergonomics.** One exported helper for the read, imported everywhere, so deduplication works and an audit is a search rather than a reading exercise. 5. **Then blast radius.** Reading identity in a layout that wraps most of the app quietly makes a large surface request-dependent. Push the personal part down to the smallest subtree that genuinely needs it. ## The organisational angle On a large codebase, the property that matters is not which location is cleverest but which convention survives fifty contributors. A single documented helper and a rule that authoritative checks live with routes and writes is auditable: you can enumerate every surface that depends on identity, and a review can see the check in the same file as the data it guards. A scheme where protection is implied by directory position is fast to write and fails silently the first time someone adds a route in the wrong place or reorganises the tree. ## Failure modes to watch for - A rule enforced only before routing, bypassed by an internal navigation that never issues the request that step runs on. - A rule enforced only in a layout that persists across navigations and therefore does not re-evaluate. - Five private session helpers, so deduplication never fires and each page load makes five lookups. - The early step made authoritative and then made expensive, so every anonymous request pays for it.

  • If the pre-routing step already resolved the session, how does the render get it without reading again?
    Only if you pass it deliberately — attaching it to a request-scoped value the render can read, or forwarding it on the request. Both couple the two layers and make the render depend on a step that may not run in every environment. Most teams let the render read again and rely on per-request deduplication to make it one lookup.
  • Why is a check that lives only in a step before routing not a sufficient control?
    Because it only runs on requests that reach it. A client-side navigation that fetches just the route's data, or any path that does not match its pattern, skips it entirely. It is excellent as a uniform, cheap bounce; the check that must hold belongs next to the data being read or the write being performed.
  • How small should the personalised subtree be?
    As small as the feature honestly requires. Every module that reads identity makes its output about one person, so pushing the read down to the header widget or the one panel that needs it keeps the rest of the page produced once for everyone. Reading it in a layout near the root makes almost the whole app request-dependent by default.

saying these in an interview costs you the question

  • Assumes a layout's session read is handed to the route below it
  • Treats a pre-routing check as the only control needed
  • Believes the early step knows which route will render
  • Puts an expensive session lookup on every anonymous request
  • Lets each module write its own private session helper
  • Reads identity near the root, making the whole app request-dependent