skip to content

A team wants Next.js middleware to load the signed-in user's full profile from the database on every request, so that pages do not have to fetch it themselves. Why is that the wrong surface, and where does the work belong?

level: seniorimportance: should knowfreq 45%

answer

  1. paid on every matched request
  2. runs before the route is known
  3. no props, only headers and cookies
  4. strings at the seam, types lost
  5. fetch where the data is rendered

basics

~20 s

Middleware sits on the critical path of every request its matcher selects, so the query is paid even by requests that never use the profile, and it has no way to hand an object to the page. Fetch the profile in the server component tree instead.

solid answer

~50 s

Two reasons. First, cost: middleware runs for every request its matcher selects — page navigations, prefetches, nested data requests, asset-adjacent paths — so a database round trip there is multiplied across traffic that never looks at a profile, and it lands before any of the work that actually produces the page. Second, plumbing: middleware has no channel into the component tree. It can only pass what it learned through headers, cookies or a rewritten URL, so the team ends up serialising a domain object into a string and re-parsing it downstream, losing types and any freshness guarantee. The fetch belongs where it is used — in a layout or the component that renders the profile — where it is typed, colocated, deduplicated within a render and cacheable. Middleware should keep only decisions that must happen before a route is chosen.

go deeper

for a junior

Know that middleware runs on incoming requests and cannot hand data to a page as props, so a page's data is fetched in the page or layout that renders it.

for a middle

Explain the mechanics: the matcher decides how often middleware runs, its only outputs are the response and the request envelope, and passing an object downstream means serialising it into a header or cookie.

for a senior

Show the production reasoning — uniform latency added ahead of rendering, an undocumented cache with no invalidation, types lost at the header seam — and name the alternative placement in the server tree.

for a principal

Own the guardrail. Decide what is allowed in middleware at all, since it is shared by every team and every route, and set the review rule that keeps per-feature data loading from accumulating there.

## The instinct and why it is wrong "Middleware runs before everything, so let's do the common work there" sounds like a good deal. It is really two mistakes wearing one coat: a cost mistake and a data-flow mistake. ## Mistake one: you pay for it everywhere Middleware is the only Next.js surface that runs without the request naming it. Whatever its matcher selects goes through it — full page loads, client-side navigations, prefetch requests the router issues speculatively, and requests for other routes entirely. A database query there is not "once per page view"; it is once per matched request, and it sits in front of the work that actually produces a response, so its latency is additive for every one of them. The cost is also invisible in the places engineers look. A slow query in a component shows up next to the component. The same query in middleware shows up as "the whole site got slower", and it stays that way even for routes that never touch a profile — the marketing page, the health check, the asset-adjacent path someone forgot to exclude. ## Mistake two: there is no channel to the page Even if the cost were acceptable, middleware cannot give the page an object. It returns a response, not props. The only ways to pass what it learned downstream are the request/response envelope — setting a header or a cookie — or rewriting the URL so the path carries the value. Every one of those is a string. So the team's profile becomes a JSON string in a header, parsed back in a layout with no type safety at the seam, subject to header size limits, and duplicated into every downstream hop. If the profile changes mid-session, whatever went into a cookie is now stale until something rewrites it, and you have built a small, undocumented cache with no invalidation story. ## Where the work actually goes Fetch the profile where it is rendered. A Server Component — often a layout that wraps the authenticated area — can call the data layer directly. That gives you the type at the call site, colocation with the markup that consumes it, per-render deduplication when several components ask for the same thing, and access to the framework's caching and revalidation machinery when you want the result to persist beyond one render. If several branches of the tree need it, a small server-side accessor function that the framework can dedupe is the idiomatic shape. ```ts // app/(app)/layout.tsx import { getCurrentUser } from '@/lib/auth' export default async function AppLayout({ children }: { children: React.ReactNode }) { const user = await getCurrentUser() return <UserProvider value={user}>{children}</UserProvider> } ``` ## What middleware is genuinely for Keep it to decisions that must be made before a route is chosen and that need nothing more than the request envelope: - redirecting or rewriting based on the path, a cookie or a header; - attaching a locale, experiment bucket or correlation id; - setting response headers that apply broadly; - coarse gating of whole sections of the site. The test to apply: *does this decision have to happen before Next knows which route it is, and can it be made from the request alone?* If yes, middleware. If it needs the route's data, produces content, or only matters for one segment, it belongs downstream. ## The nuance worth voicing There is a defensible middle ground: a cheap check on data the request already carries — reading a session cookie's presence, decoding a token you can verify without I/O — to decide a redirect. That is a decision, not a data load, and it stays inside middleware's job. The line is between *deciding something about the request* and *fetching something the page needs*. Loading a full profile is unambiguously the second.

  • Is there any version of "look at the user in middleware" that is legitimate?
    Yes, when it is a decision rather than a data load. Reading whether a session cookie is present, or verifying a token you can check without I/O, to decide a redirect is exactly middleware's job: it needs only the request, and it must happen before a route is chosen. The line is crossed when you start fetching the record the page will render.
  • If several parts of the server tree need the same profile, are you not fetching it repeatedly?
    Not in practice. Within a single render pass the framework deduplicates identical server-side reads, so a shared accessor called from a layout and two components hits the data source once. That is a property you get in the server tree and specifically do not get by stuffing the value into a header from middleware.
  • What is the symptom a team notices before they realise middleware is the problem?
    Uniform latency. Every route gets slower by roughly the same amount, including ones with no data of their own, and the slowdown appears in time-to-first-byte rather than in any component's timing. Because middleware runs ahead of the rendering work, its cost hides from the per-route measurements engineers usually reach for first.

saying these in an interview costs you the question

  • Middleware can return data to the page as props
  • Middleware runs once per page view, not per request
  • Header-passed JSON is a fine substitute for fetching in the tree
  • Doing it early means it is cheaper overall
  • Middleware is a good place to cache per-user data

context