Nuxt 4 Patterns
Credit
Original skill by ECC.
Use this skill when you build or fix a Nuxt 4 app.
Use This Skill For
- Server HTML that does not match the first browser render
- SSR and browser-only code
- Static pages, SWR, ISR, and route rules
- Slow pages or large page data
- Lazy loading and lazy hydration
useFetch,useAsyncData, and$fetch- Bugs tied to route params or middleware
Safe Hydration
The server and browser must make the same HTML on the first render.
- Do not put
Date.now()orMath.random()in server-rendered HTML. - Do not read
window,document, storage, or browser size during SSR. - Move browser-only work into
onMounted(). - You may also use
import.meta.client,<ClientOnly>, or a.client.vuefile. - Use Nuxt's
useRoute(). Do not importuseRoutefromvue-router. - Do not use
route.fullPathto build server HTML. The URL hash only exists in the browser. - Do not hide a mismatch with
ssr: falseunless the whole route needs the browser. - Use one time zone and locale when text must match on both sides.
- Check for bad HTML, such as a
<div>inside a<p>. The browser may change it. - Give lists stable and unique
:keyvalues. - Do not show user data from browser storage during the first render. Read it after mount.
Fetch Data Safely
Use await useFetch() for a simple API read in a page or component. Nuxt can pass the server result to the browser. This helps stop a second request.
const { data, status, error } = await useFetch('/api/products')Use useAsyncData() when you need:
- More than one data source
- A custom data function
- A stable cache key
- Data from a library other than
$fetch
const route = useRoute()
const slug = computed(() => String(route.params.slug))
const { data: article, status, error, refresh } = await useAsyncData(
() => `article:${slug.value}`,
() => $fetch(`/api/articles/${slug.value}`),
{
watch: [slug],
},
)Follow these rules:
- Make each cache key stable and unique.
- Put route params in the key when the data depends on them.
- Add
watchwhen the same page can stay open while a param changes. - Return real data from the handler. Do not return
undefined. - Keep the handler free of side effects. Do not send mail, write data, or track events in it.
- Use
$fetch()for form saves, button actions, and other user actions. - Do not use top-level
$fetch()for page data that must come from SSR. - Use
lazy: true,useLazyFetch(), oruseLazyAsyncData()for data that can load later. - Show a loading state when
status === 'pending'. - Show a useful error state when a request fails.
- Use
server: falseonly when the first page view and search tools do not need the data. - Use
pickto keep only fields the page needs. - Use shallow data when deep updates are not needed.
- Use
runtimeConfigfor private values. Never send private keys to the browser. - Read request headers and cookies with Nuxt server tools when SSR needs them.
Route Rules
Set page render and cache rules in nuxt.config.ts.
export default defineNuxtConfig({
routeRules: {
'/': { prerender: true },
'/products/**': { swr: 3600 },
'/blog/**': { isr: true },
'/admin/**': { ssr: false },
'/api/**': { cache: { maxAge: 3600 } },
},
})prerendermakes HTML at build time.swrserves saved content, then updates it in the back end.isrrebuilds saved pages on hosts that support it.ssr: falserenders the route in the browser.cachesets rules for server replies.redirectsends the user to another path.
Pick rules by route group. A shop page, blog, admin page, and API may each need a different rule.
Check these edge cases:
- Do not cache private or user-only data in a shared cache.
- Make sure the host supports the chosen ISR rules.
- Give content a clear refresh time.
- Keep admin routes out of static page builds when they need live user data.
- Test route patterns so broad rules do not catch the wrong pages.
Lazy Loading
Nuxt already splits page code by route. Keep route boundaries clear before you split small components.
Use the Lazy name prefix for parts that are not needed at once.
<template>
<LazyRecommendations v-if="showRecommendations" />
<LazyProductGallery hydrate-on-visible />
</template>- Use
v-ifso a lazy part is not loaded before it is needed. - Use lazy hydration for parts below the first screen.
- Use
hydrate-on-visiblefor a part that can wait until it is seen. - Use
defineLazyHydrationComponent()for a custom visible or idle rule. - Lazy hydration works with Vue single-file components.
- A new prop can cause a lazy component to hydrate at once.
- Use
<NuxtLink>for links inside the app. - Keep key content and main actions out of delayed parts.
- Test that delayed controls still work with a keyboard.
Concrete Example
This page loads a product on the server. Reviews load later in the browser.
<script setup lang="ts">
const route = useRoute()
const id = computed(() => String(route.params.id))
const {
data: product,
status: productStatus,
error: productError,
} = await useAsyncData(
() => `product:${id.value}`,
() => $fetch(`/api/products/${id.value}`),
{
watch: [id],
pick: ['id', 'name', 'price'],
},
)
const {
data: reviews,
status: reviewStatus,
error: reviewError,
} = await useFetch(
() => `/api/products/${id.value}/reviews`,
{
lazy: true,
server: false,
watch: [id],
},
)
</script>
<template>
<main>
<p v-if="productStatus === 'pending'">Loading product...</p>
<p v-else-if="productError">Could not load this product.</p>
<article v-else-if="product">
<h1>{{ product.name }}</h1>
<p>{{ product.price }}</p>
</article>
<section>
<h2>Reviews</h2>
<p v-if="reviewStatus === 'pending'">Loading reviews...</p>
<p v-else-if="reviewError">Could not load reviews.</p>
<ul v-else>
<li v-for="review in reviews" :key="review.id">
{{ review.text }}
</li>
</ul>
</section>
</main>
</template>Review List
Before you finish, check that:
- The server and first browser render make the same HTML.
- Browser-only code runs only in the browser.
- Page reads use
useFetchoruseAsyncData. - Data keys include values that change the result.
- Loading, empty, and error states are shown.
- Private data is not put in a shared cache.
- Route rules match search and freshness needs.
- Heavy parts load or hydrate later.
- Main text and actions are ready at first load.
- Dynamic route changes load the right data.