Rendering and data are where frontend architecture meets infrastructure, and where Nuxt gives you the most knobs: universal, static, swr, isr, SPA, islands and streaming per route; Nitro caching and storage; edge or regional deployment. The design skill is choosing per route and per data type with explicit criteria, and knowing what each choice costs.
swr/isr (public, changing, shareable by URL), SSR without cache (query-dependent or lightly personalised, still crawlable), SPA (ssr: false; private, interaction-heavy, not indexed), server components/islands for heavy static fragments inside any mode, lazy hydration for below-the-fold interactivity, streaming where TTFB on slow data matters (experimental).Vary) → Nitro response cache (swr, defineCachedEventHandler) → function cache (cachedFunction) → application caches (Redis) → database (query cache, replicas). Each has a key, a TTL and an invalidation path; personalised data lives only in private layers. The server caching page has the mechanics.Promise.all or by hoisting keys. Payload size is a budget (pick, DTOs).ETags for slow data; versioned messages, a pub/sub backplane for multiple instances, and authentication at the handshake (realtime page).HttpOnly first-party session cookie readable by SSR and forwarded to the BFF; identity provider via OIDC on the server; authorisation on every server request; the edge can validate sessions for static-plus-private splits. Personalised HTML never shares a cache key with public HTML.event.context.tenant; per-tenant tokens (data-brand or --ui-* values) rather than per-tenant components; tenant in every cache key and every query (or row-level security); per-tenant feature flags and limits.hreflang, lazy-loaded message files, per-locale prerendering or swr, Intl formatting with locale and time zone from shared inputs, content translated at the source (CMS or content directories), RTL through logical properties (i18n page).stale-if-error, error boundaries per section so one failing widget does not take the page, graceful degradation of realtime to polling, queue-backed writes for third-party outages, and idempotent retries.window access, module-level state, non-deterministic render; adopt route by route with ssr: false as the default and flip routes over), monolith → BFF (introduce Nitro routes in front of the old API and move shaping there), Nuxt 3 → 4 → 5 (codemods, compatibility versions), REST → typed procedures (oRPC alongside REST), region → edge for the static half first.The decision table, then the configuration that implements it:
Route Crawl? Personal? Freshness Mode Cache key / notes
/ yes no deploy prerender CDN; HTML must-revalidate
/blog/** yes no hourly swr 3600 URL; purge on publish via webhook
/c/**, /p/** yes no minutes swr 300 URL + locale + currency (Vary)
/search yes* no live SSR, no HTML cache cachedFunction per normalised query, 30s
/cart, /checkout no yes live SSR private no shared cache; session cookie
/account/** no yes live SPA (ssr: false) API private, no-store
/live/** no yes realtime SSR shell + SSE versioned events; Redis pub/sub
export default defineNuxtConfig({
routeRules: {
'/': { prerender: true },
'/blog/**': { swr: 3600 },
'/c/**': { swr: 300 },
'/p/**': { swr: 300 },
'/search': { headers: { 'cache-control': 'private, no-store' } },
'/cart': { headers: { 'cache-control': 'private, no-store' } },
'/checkout/**': { headers: { 'cache-control': 'private, no-store' } },
'/account/**': { ssr: false, appMiddleware: 'auth' },
'/api/catalog/**': { cache: { maxAge: 300, swr: true, varies: ['accept-language'] } },
'/api/account/**': { headers: { 'cache-control': 'private, no-store' } },
},
nitro: { storage: { cache: { driver: 'cloudflare-kv-binding', binding: 'CACHE' } } },
})
Tenant resolution and a cache key that respects tenancy and locale:
export default defineEventHandler(async (event) => {
const host = getRequestHost(event)
event.context.tenant = await resolveTenant(host) // cached: subdomain -> tenant id, brand tokens, feature flags
if (!event.context.tenant) throw createError({ status: 404, statusText: 'Unknown tenant' })
})
export default defineCachedEventHandler(async (event) => {
const { tenant } = event.context
return listFeatured(useDb(), { tenantId: tenant.id, locale: getLocale(event) })
}, {
name: 'featured',
maxAge: 300,
swr: true,
getKey: event => `${event.context.tenant.id}:${getLocale(event)}`, // never a shared entry across tenants
})
Eliminating a client waterfall with server-side aggregation and a single payload:
<script setup lang="ts">
const { params } = useRoute()
// One server-side aggregation instead of product -> then reviews -> then related in the browser.
const { data } = await useFetch(`/api/pdp/${params.slug}`) // BFF route runs Promise.all over product, reviews summary, related
// Personalised, uncacheable part fetched client-side after hydration:
const { data: stock } = useLazyFetch(() => `/api/stock/${params.slug}`, { server: false })
</script>
Resilience in the BFF route the page relies on:
export default defineEventHandler(async (event) => {
const slug = getRouterParam(event, 'slug')!
const [product, reviews, related] = await Promise.all([
getProduct(useDb(), slug), // required: a failure is a 404/500
getReviewSummary(slug).catch(() => null), // optional: degrade the section
getRelated(slug, { signal: AbortSignal.timeout(800) }).catch(() => []), // optional and bounded
])
if (!product) throw createError({ status: 404, statusText: 'Not found' })
return { product: toProductDto(product), reviews, related }
})
Moving SSR to the edge while every request makes four database round trips to Frankfurt makes pages slower for users in Frankfurt and barely faster elsewhere. Cache at the edge, batch queries, use an accelerator like Hyperdrive, or keep data-heavy routes regional and put only static and cached pages on the edge.
A cached handler keyed by URL on a multi-tenant host serves tenant A's featured products to tenant B. The tenant belongs in every cache key and every query, and a test that requests the same URL for two tenants belongs in the suite.
Current routeRules options (swr, isr, cache.varies, appMiddleware), the status of experimental SSR streaming, and Hyperdrive's supported databases.
routeRules in a playground; verify each row with curl -I."Per route group, with five criteria: whether crawlers need it, whether it is personalised, how fresh it must be, how expensive it is to render and how interactive it is afterwards. Public and stable pages are prerendered and served by the CDN; public pages that change get swr or isr so they render once per interval; query-dependent pages render on the server without HTML caching but with cached functions underneath; personalised pages render privately or as an SPA behind auth. Caching is layered from the browser through the CDN and Nitro to the database, each with a key that includes everything that changes the output, tenant and locale included, and personalised data never shares a key. The server aggregates upstream calls into one payload so the client has no waterfalls, and I decide up front what to measure per route so the design can be checked against numbers."
"The edge for what is static or cacheable and for request-light logic: prerendered pages, swr HTML, session validation, redirects, small BFF calls; users get low latency and it scales to zero. Regional Node servers for data-heavy routes that make several database queries, for long-lived connections and for CPU-heavy work, because the database lives in a region and every query from the edge pays a round trip. In practice a hybrid: the CDN and edge serve the cached half, the regional server handles the personalised half, and where edge rendering must touch the database, a connection accelerator such as Hyperdrive and read replicas reduce the penalty."
Nothing yet. Notes filed here show up on this page and the section overview.
Application structure and boundaries
Organising a large Nuxt codebase — by type versus by feature and how Nuxt's scanning constrains that, enforced dependency direction with lint rules, the shared kernel, server services versus handlers, monorepos with pnpm workspaces and Turborepo, Nuxt layers as the unit of reuse, and when micro-frontends are justified.
Worked designs
Four complete design-round answers in interview shape — an e-commerce catalogue and product page, a realtime analytics dashboard, a multi-tenant SaaS admin with a content site, and a file upload and processing pipeline — each with requirements, architecture, data and API, state placement, the hard part, cross-cutting concerns, trade-offs and what breaks at scale.