Building Nuxt apps

Routing and pages

File-based routing in Nuxt 4, definePageMeta, layouts, route middleware on server and client, NuxtLink prefetching, typed routes and the route rules that live in Nitro.

Routing questions test whether you understand that Nuxt routes exist in two places: Vue Router in the app, and Nitro's route rules on the server. Middleware running on the server for the first request and on the client afterwards, a redirect that must set a status code, a protected route that is "protected" only in the browser: these are the scenarios interviewers build questions around.

Know

  • Files are routes. app/pages/index.vue → /, about.vue → /about, users/[id].vue → /users/:id, [...slug].vue catch-all, [[id]].vue optional param, (marketing)/pricing.vue route groups that organise files without affecting the URL. A users.vue next to a users/ folder is a parent route that renders children through <NuxtPage>.
  • definePageMeta is compiled out of the component and attached to the route record: layout, middleware, name, path, alias, redirect, keepalive, key, validate (return false for a 404 or an error object), scrollToTop, pageTransition/layoutTransition. It cannot reference component-local variables because it is extracted at build time.
  • Layouts live in app/layouts/, wrap pages through <NuxtLayout> in app.vue, and are chosen per page (layout: 'admin') or switched at runtime with setPageLayout. layout: false renders the page bare.
  • Route middleware (app/middleware/): named files are opted into per page, *.global.ts run on every navigation in alphabetical order, inline functions go straight into definePageMeta. They run on the server for the initial request and in the browser for client-side navigations, return navigateTo(...) to redirect or abortNavigation(error) to stop, and receive to/from. Plugins can add middleware dynamically with addRouteMiddleware.
  • Middleware is UX, not security. A client-side redirect from /admin protects nothing; the API must check the session itself. Middleware exists so users do not see a page they cannot use.
  • navigateTo(to, { replace, redirectCode, external, open }) works in middleware, plugins and components; on the server it sets the status (default 302; use redirectCode: 301 for permanent). external: true is required for off-site URLs, which is also the guard against open redirects.
  • <NuxtLink> renders <a> with client-side navigation and prefetches the target page's chunk and payload when the link enters the viewport (prefetchOn: 'visibility' default, or 'interaction'; noPrefetch to opt out). It handles external links, target, active classes and trailing slashes; useRouter().push is for programmatic navigation.
  • Typed routes (experimental.typedPages, default in Nuxt 5 Nuxt 5 ) generate a route map with unplugin-vue-router, so navigateTo({ name: 'users-id', params: { id } }) and useRoute('users-id') are type-checked.
  • Route rules are Nitro-level configuration keyed by path pattern: redirect, headers, prerender, swr, isr, ssr: false, cors, cache, appMiddleware. They apply before the Vue app exists, so a redirect rule answers without rendering. Hybrid rendering is configured here, see rendering strategies.
  • Scrolling and keys. app/router.options.ts customises scrollBehavior; <NuxtPage :page-key> or definePageMeta({ key }) controls when a page component is remounted versus reused when only params change.

How it works

Auth middleware done properly: the server-side check redirects with a status code, the page opts in, and the API stays the real gate:

app/middleware/auth.ts
export default defineNuxtRouteMiddleware((to) => {
  const { loggedIn } = useUserSession() // nuxt-auth-utils, works on server and client
  if (!loggedIn.value) {
    return navigateTo({ path: '/login', query: { redirect: to.fullPath } }, { redirectCode: 302 })
  }
})
app/pages/account/index.vue
<script setup lang="ts">
definePageMeta({
  middleware: 'auth',
  layout: 'account',
  // 404 for anything that is not a numeric id on sibling routes: validate runs before the page renders
})
</script>
server/api/account/orders.get.ts
export default defineEventHandler(async (event) => {
  const { user } = await requireUserSession(event) // 401 if missing: this is the actual protection
  return useDb().orders.forUser(user.id)
})

A parent route with nested children, and a link that prefetches on hover rather than on sight:

app/pages/users.vue
<template>
  <div class="grid grid-cols-[16rem_1fr]">
    <nav>
      <NuxtLink
        v-for="u in users"
        :key="u.id"
        :to="{ name: 'users-id', params: { id: u.id } }"
        prefetch-on="interaction"
        active-class="font-semibold"
      >{{ u.name }}</NuxtLink>
    </nav>
    <!-- users/[id].vue renders here -->
    <NuxtPage />
  </div>
</template>

Route rules for the parts of the site that should not be rendered per request:

nuxt.config.ts
export default defineNuxtConfig({
  routeRules: {
    '/': { prerender: true },
    '/blog/**': { swr: 600 }, // rendered once, served stale for 10 minutes, revalidated in the background
    '/admin/**': { ssr: false, appMiddleware: 'auth' }, // SPA area, middleware attached without touching the pages
    '/old-docs/**': { redirect: { to: '/docs/**', status: 301 } },
    '/api/public/**': { cors: true, headers: { 'cache-control': 'public, max-age=60' } },
  },
})
Gotcha· A client-side guard is not authorisation

Middleware runs in the browser after the first load. Anyone can skip it with fetch('/api/admin/users'). Every protected server route must verify the session itself; middleware only decides what the user sees.

Gotcha· definePageMeta cannot see your variables

It is extracted at compile time, so definePageMeta({ layout: isAdmin ? 'admin' : 'default' }) fails. Use setPageLayout at runtime, or middleware that reads the route.

Verify before the interview:

The prefetchOn values and defaults, plus any 4.5 additions to NuxtLink (such as named views and useLayout() mentioned in the 4.5 release notes). Confirm before naming them.

docs ↗

Exercise

Exercise
  • Build users.vue with nested users/[id].vue, add validate that 404s on non-numeric ids, and confirm the 404 status with curl -I.
  • Write a global middleware that logs to.path with import.meta.server ? 'server' : 'client'; load a page directly, then navigate to it, and compare.
  • Add the routeRules above, run nuxt build and node .output/server/index.mjs, and check response headers on /blog/x twice to see the cache behaviour.

Be able to say

Be able to say· How does route middleware work in Nuxt, and what is it for?

"Route middleware runs before a navigation resolves: on the server for the very first request and in the browser for every client-side navigation afterwards. Global middleware runs on every route in alphabetical order, named middleware is opted into per page through definePageMeta, and it can redirect with navigateTo, including a proper status code on the server, or stop with abortNavigation. Its job is user experience: sending an anonymous user to the login page, or picking a layout. It is not authorisation, because in the browser it can be bypassed with a direct API call, so every protected server route checks the session itself."

Be able to say· What is the difference between route middleware and route rules?

"Middleware is Vue Router-level and runs in the Vue app on both sides of the wire; route rules are Nitro-level configuration keyed by path pattern that applies before the app exists. So a redirect in route rules answers instantly with no render, headers and caching (swr, isr, prerender) are route rules, and turning a section of the site into an SPA with ssr: false is a route rule too. Route rules can also attach middleware to a path pattern with appMiddleware, which is handy for layers. I reach for route rules whenever the decision does not need application state."

Your notes

Open in Notes

Nothing yet. Notes filed here show up on this page and the section overview.