Nuxt 3 Hybrid Rendering: Route Rules as Trust Boundaries
Nuxt 3 route rules make rendering mode an explicit, auditable property per route. This architecture note covers trust boundaries, operational verification, and failure modes.
25 Apr 2026, 23:09 UTC

Requirements: Why Per-Route Rendering Decisions Matter
Modern applications rarely fit a single rendering model. A marketing page needs CDN-cached static HTML for global latency. A dashboard requires per-request SSR with access to private environment variables and database connections. A blog post benefits from stale-while-revalidate (SWR)—fast cached reads with background freshness. Nuxt 3 expresses these requirements through route rules in nuxt.config.ts, making the rendering mode an explicit, auditable property of each route rather than a global framework setting.
The core requirement is to align each route's rendering mode with its data sensitivity, freshness needs, and operational constraints. A misaligned rule can leak secrets into static builds, serve stale personalized content, or crash at the edge because a Node-only API was used in an edge-preset SSR route.
Smallest Suitable Design: The Route Rule Matrix
Route rules are a map from route patterns to rendering modes. The minimal useful configuration declares the mode for each route segment:
// nuxt.config.ts
export default defineNuxtConfig({
routeRules: {
'/': { prerender: true }, // Static at build
'/blog/**': { swr: 3600 }, // SWR with 1hr TTL
'/dashboard/**': { ssr: true }, // SSR per request
'/api/revalidate': { isr: { secret: '...' } } // ISR endpoint
}
})Each mode defines a trust boundary:
- prerender (static): HTML generated at build. Trust boundary = build pipeline. Secrets must be injected via build-time env or
runtimeConfigwithpublickeys only. - swr: Cached HTML served from CDN/edge; background revalidation after TTL expires. Trust boundary = build + revalidation job. Same secret constraints as static.
- ssr: Server executes per request. Trust boundary = runtime server. Full access to private
runtimeConfig, filesystem, and database. - isr: On-demand revalidation via signed webhook. Trust boundary = revalidation secret. The endpoint is public—protect with HMAC or shared token.
Data fetching composables (useFetch, useAsyncData) automatically respect the route rule. On an SSR route, the fetch runs on the server with access to private env. On a static/SWR route, it runs at build or in the background revalidation worker—meaning any private tokens must be available in those environments.
Trust and Data Boundaries in Practice
Private Data on Static Routes: A Concrete Misconfiguration
Consider a route /admin/reports mistakenly set to prerender: true. The build runs useFetch('/api/admin/reports') which reads process.env.DATABASE_URL. Two failures occur:
- The build fails if
DATABASE_URLisn't set in the CI environment. - If the build succeeds (e.g., CI has the var), the rendered HTML contains real report data—now cached on CDN edges globally. Any user hitting the static page sees that data.
Fix: Set ssr: true for any route accessing non-public runtime config or private APIs. Audit with the nitro:route-rules virtual module at runtime:
// plugins/audit-rules.ts
export default defineNuxtPlugin(() => {
const rules = useNuxtApp().$nitro.routeRules
Object.entries(rules).forEach(([pattern, rule]) => {
if (rule.prerender || rule.swr) {
console.warn(`[audit] Static/SWR route: ${pattern}`, rule)
}
})
})Edge Preset Compatibility
When deploying to Cloudflare Pages, Vercel Edge, or Netlify Edge Functions, the output preset imposes CPU/memory limits and removes Node.js APIs (fs, crypto, native DB drivers). An SSR route using prisma or fs.readFileSync will crash at request time.
| Route Type | Node Preset | Edge Preset |
|---|---|---|
| SSR with DB (Prisma, pg) | ✅ Supported | ❌ Fails — use Node preset or move DB to API route |
| SSR with fetch-only | ✅ Supported | ✅ Supported |
| Static/SWR/ISR | ✅ Supported | ✅ Supported (offloaded to CDN) |
If your SSR routes need Node APIs, choose the node-server preset or split the architecture: keep SSR on Node and push static/SWR to the edge via nitro.preset: 'cloudflare-pages'.
Operational Checks: Verifying the Deployed Behavior
1. Confirm Effective Rules at Build
Run npx nuxi build --analyze and inspect .output/server/routes.json. Each route entry includes its resolved rule. Example snippet:
{
"routes": [
{ "route": "/", "rule": { "prerender": true } },
{ "route": "/blog/_slug", "rule": { "swr": 3600 } },
{ "route": "/dashboard/**", "rule": { "ssr": true } }
]
}2. Observe Runtime Rendering Mode
Nitro adds an x-nuxt-render-mode header to responses. In staging, use curl to verify:
curl -I https://staging.example.com/blog/my-post
# x-nuxt-render-mode: swr
curl -I https://staging.example.com/dashboard
# x-nuxt-render-mode: ssr3. Test Revalidation Latency and Correctness
For SWR: wait past TTL, then request. The first request after TTL serves stale HTML while triggering background revalidation. The second request gets fresh HTML.
For ISR: POST to the revalidation endpoint with the secret:
curl -X POST https://staging.example.com/__nuxt_revalidate \
-H "Content-Type: application/json" \
-d '{"secret":"your-secret","paths":["/blog/test"]}'Failure Modes and Their Signatures
| Mode | Failure Point | User Impact | Observability |
|---|---|---|---|
| SSR | Request time (uncaught error) | 5xx response | Logs, Sentry |
| Static | Build time (prerender error) | Deploy blocked | CI logs |
| SWR | Background revalidation error | Stale content persists | Revalidation logs |
| ISR | Webhook signature mismatch | Stale content persists | Webhook logs |
Cache-key collision: If route rules omit vary headers for locale or user segments, the CDN may serve one cached variant to all. Example: /products with swr: 3600 but no vary: accept-language—French users see English HTML. Fix: add headers: { 'vary': 'accept-language' } to the rule.
Conditions That Would Change the Design
- Unified rendering model mandated: If organizational policy requires all routes SSR (for auth simplicity) or all static (for cost), route rules become a migration tool rather than a permanent architecture.
- Edge-first preset with Node fallback: If hybrid presets allow per-route runtime selection, the trust boundary shifts from the global preset to the per-route runtime.
- Cache-tag invalidation: If the CDN supports surrogate keys (e.g., Cloudflare Cache-Tags), ISR/SWR can invalidate by tag instead of path, changing the architecture from path-based to tag-based.
Limitations
- Route rules are evaluated at build for static/SWR; changing a rule requires a rebuild and redeploy.
- TypeScript types for rules come from config; runtime modifications via Nitro hooks (
addRouteRule) are not type-checked. UsedefineRouteRulesfor typed overrides. prerender: { crawlLinks: true }(Nuxt 3.8+) discovers routes by crawling<a>links. If the link graph is incomplete, missing routes return 404 at build.- ISR revalidation endpoints are public by default. Always require a signed payload or secret header.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.