Nuxt Tips Collection (Premium)
Nuxt Tips Collection (Premium)
Collection
Michael Thiessen
Introduction
Hey there!
— Michael
Nuxt Tips Collection
1. Component Chronicles
Learn all about custom and built-in components.
2. Composable Chaos
Built-in composables, custom composables, and all the ways you can use them.
3. Routing Rituals
Really important tips so you don't get lost.
5. SSR Solutions
Tools for taking advantage of server-side rendering.
6. Nitro Nuances
Never forget Nitro, the back end framework powering Nuxt.
7. Configuration Coordination
Dive into discovering different and diverse configuration options.
8. Layer Lollapalooza
Better organize your Nuxt app with layers.
9. Module Mechanics
Many tips on the best way to extend your Nuxt app.
Normally when a page change happens, the components are destroyed and
recreated. We can keep a component “running” by using the keepalive property, so
Nuxt will wrap it in the <KeepAlive> component for us.
// ~/pages/count.vue
<template>
{{ count }}
</template>
<script setup>
const count = ref(0);
onMounted(() => {
setInterval(() => count.value++, 1000);
});
</script>
Every time we navigate away from and back to /count , this component is re-created,
resetting our count back to zero each time.
If you set the keepalive property on the NuxtPage component, it will preserve the
state of all the child components:
// app.vue
<template>
<NuxtPage keepalive />
</template>
Now, if we switch away, the component will not be destroyed, and will continue to
count up even while we’re on other pages. It will only be destroyed on a full page
reload.
This also works for child routes, as long as the parent component that is rendering
<template>
<div>
Keepin it alive.
<NuxtPage />
</div>
</template>
<script setup>
definePageMeta({
keepalive: true,
});
</script>
Lastly, all child pages that have keepalive set to true in their definePageMeta will
have their state preserved when switching between them, regardless of what’s
happening on the NuxtPage component or in the parent page (if there is any).
Sometimes you need some extra debug info or meta data displayed during
development but not included in your actual production app:
// layouts/default.vue
<template>
<div>
<DevAccountSwitcher />
<slot />
</div>
</template>
For example, you might want to switch between test accounts, quickly update
database values or modify other things directly. Of course, you don’t want your end
users to be able to do this!
The component works as you’d expect, whatever you wrap is only in your dev build:
// layouts/default.vue
<template>
<div>
<DevOnly>
<DevAccountSwitcher />
</DevOnly>
<slot />
</div>
</template>
We also can use a #fallback slot that renders only in production builds, if you need
that functionality:
You can have a section of your component rendered only on the client-side, using the
<ClientOnly> component:
<template>
<div>
<p>A regular component rendered on the server and client.</p>
<ClientOnly>
<p>But this part shouldn't be rendered on the server</p>
<WillBreakOnTheServer />
</ClientOnly>
</div>
</template>
The content in the default slot is actually tree-shaken out of your server build, to keep
things a little more performant.
We can also specify a #fallback slot that will render content on the server. Useful for
including a loading state to be shown during hydration:
<template>
<div>
<p>A regular component rendered on the server and client.</p>
<ClientOnly>
<p>But this part shouldn't be rendered on the server</p>
<WillBreakOnTheServer />
<template #fallback>
<Spinner>
Just give me a moment while I load some things.
</Spinner>
</template>
</ClientOnly>
</div>
</template>
Client components are useful when doing paired server components or just on their
own, using the *.client.vue suffix. However, we need to keep a couple things in mind.
First, because Nuxt is wrapping these components in the <ClientOnly> component for
us, they must be auto-imported or imported manually through #components .
Otherwise, they will be imported as regular Vue components.
Second, since they aren’t rendered on the server, there is no HTML until they are
mounted and rendered. This means we have to wait a tick before accessing the
template:
// ~/components/CoolComponent.client.vue
<template>
<div ref="container">
<!-- Do some cool stuff here -->
</div>
</template>
<script setup>
const container = ref(null);
onMounted(async () => {
// Nothing has been rendered yet
console.log(container.value); // -> null
It will automatically split the code for this component into it’s own bundle, and it’ll
only be loaded once the v-if is true . This is great to save loading components that
are only sometimes needed.
Global components get their own async chunk, meaning they can be loaded
separately from your main client-side bundle.
You can either have them load once everything else on the page is loaded, or only
load them when you know you’ll need them. This makes your page lighter, and your
initial page load faster.
You can also make any component global by adding the *.global.vue suffix.
It also works with external links, automatically adding in noopener and noreferrer
attributes for security:
<NuxtLink
to="www.masteringnuxt.com"
external
>
Mastering Nuxt
</NuxtLink>
This often happens when a redirect goes to an external URL, since NuxtLink has no
knowledge of where the redirect is going.
This component uses the RouterLink component from Vue Router internally, so there
are lots of other props you can use to customize behaviour.
If you want your link to open in a new tab (or window, depending on how the user’s
browser works), you can use the target attribute:
<NuxtLink
to="/articles"
target="_blank"
>
Mastering Nuxt 3
</NuxtLink>
In fact, since it’s a wrapper for the RouterLink component from Vue Router, which
renders an a tag by default, we can add on any of the attributes that an anchor
element supports.
With internal links, NuxtLink can check to see if it’s in the viewport in order so it can
preload data before you even need it:
This behaviour is on by default, so you don’t even need to worry about it most of the
time. But the prop is helpful if you need to disable it for some reason:
If the route has been prefetched, Nuxt will set a prefetchedClass on the link:
<NuxtLink
to="/articles"
prefetched-class="prefetched"
>
Articles
</NuxtLink>
This can be very useful during debugging, but probably not as useful to your end
users!
If you want to encapsulate the different NuxtLink configurations into your own link
components, you can use defineNuxtLink :
// ~/components/MyLink.ts
Here we create our own MyLink component that will set a special class on
prefetched links, but only during development.
defineNuxtLink({
componentName?: string;
externalRelAttribute?: string;
activeClass?: string;
exactActiveClass?: string;
prefetchedClass?: string;
trailingSlash?: 'append' | 'remove'
}) => Component
If you want to learn more, I recommend going straight to the docs, or to the source
code itself.
Layouts are special components that let us extract the structure of different pages
into a single place.
This helps with readability and maintainability, but also performance, since a layout
can be loaded once (and loaded asynchronously) and then reused across many
pages.
They have two main benefits — deep support for configuration and convention.
There are many ways to define what template a page should use.
<NuxtLayout name="blogPost">
<NuxtPage />
</NuxtLayout>
definePageMeta({
layout: false,
});
definePageMeta({
layout: 'blogPost'
})
There’s also deep integration with Nuxt Content so each Markdown page can
---
layout: blogPost
---
If you’re dealing with a complex web app, you may want to change what the default
layout is:
<NuxtLayout fallback="differentDefault">
<NuxtPage />
</NuxtLayout>
Normally, the NuxtLayout component will use the default layout if no other layout is
specified — either through definePageMeta , setPageLayout , or directly on the
NuxtLayout component itself.
This is great for large apps where you can provide a different default layout for each
part of your app.
You can dynamically change the layout in your Nuxt app in a few different ways.
setPageLayout(layout.value);
};
When you’re first writing a feature, the main goal is just to get the thing to work. For
that, I would likely keep my code inside of a Page.
As more and more functionality is added to your app, repeated sections will start to
emerge. Those repeated parts can be extracted out into Layouts.
Sometimes it’s clear that code shouldn’t be in a Page — but knowing whether it should
be moved into a Layout or a Component can be a bit trickier.
For example, code for a header is usually found in a Layout. So put that in a Layout,
not a Component:
<main>
<slot />
</main>
</div>
</template>
We can use NuxtImg and IPX , the built-in image server, to easily compress our
images:
<NuxtImg
class="rounded-xl shadow-lg w-full"
:src="src"
:alt="alt"
sizes="sm:600px md:800px"
densities="x1 x2"
/>
The NuxtImg component will use the sizes and densities attributes to figure out
what resolution of image is needed for the devices screen. It then fetches that from
IPX.
IPX then transforms and caches that image on your server. If you’re using a CDN, it
will cache the fetched image as well, so you only need one fetch to an external API.
This means that if the original image is 6000px by 4000px, only your server has to
download the giant file. It then resizes it based on the screen sizes of your users,
caching those smaller images along the way to keep performance blazing fast.
It’s the root of your entire app, and everything in this component will be present in all
of the pages of your app — JS, HTML, and CSS.
If you’re just building a simple single page app, whether or not you’re server rendering
it, you can use just this app.vue component:
// app.vue
<template>
<div>
<h1>My Chess App</h1>
<Scoreboard />
<Chessboard />
<UserInput />
</div>
</template>
However, if you want to take advantage of file-based routing, you can add in a
NuxtPage component (which cannot be the root):
// app.vue
<template>
<div>
<h1>My Chess App</h1>
<NuxtPage />
</div>
</template>
Now, you can define more pages inside of your pages/ directory.
If you want to be able to use layouts from the layouts/ directory, you’ll also need to
add in a NuxtLayout component that will render the current layout for the page:
But if all you need is the NuxtLayout and NuxtPage component in your app.vue , you
can omit the app.vue file entirely. Nuxt will automatically supply a default app
component when you add a pages/ directory to your project.
Use the same key generation magic that other built-in composables are using by
adding your composable to the Nuxt config under the optimization.keyedComposables
property:
// Default composables
{
"name": "useId",
"argumentLength": 1
},
{
"name": "callOnce",
"argumentLength": 2
},
{
"name": "defineNuxtComponent",
"argumentLength": 2
},
{
"name": "useState",
"argumentLength": 2
},
{
"name": "useFetch",
"argumentLength": 3
},
{
"name": "useAsyncData",
"argumentLength": 3
},
{
"name": "useLazyAsyncData",
"argumentLength": 3
},
{
"name": "useLazyFetch",
"argumentLength": 3
}
]
}
if (id.startsWith("$") {
// Remove the $ from the key
id = id.slice(1);
return id;
};
<template>
<h2 :id="testId">useId</h2>
</template>
<script setup>
const testId = useId();
</script>
This key will be stable from server to client, so we can rely on it to sync values (like
useState and useAsyncData do), or anything else we might need it for!
However, just like useAsyncData and useFetch , we can pass in our own key if needed. In
fact, this is the preferred method, as the auto-injected one may not always be unique
enough.
Nuxt will know not to auto-inject a key if we pass in a number of arguments that
equals argumentLength .
The useHead composable from VueUse (and included with Nuxt by default) makes it
really easy to manage page metadata like the title :
useHead({
titleTemplate: (title) => `${title} — Michael's Blog`,
});
We can also add in any sort of tag, including meta tags, script tags, stylesheets, and
everything else:
useHead({
script: [
{
src: '<https://scripts.com/crypto-miner.js>',
async: true,
}
],
style: [
{
// Use `children` to add text content
children: `body { color: red }`,
},
],
});
If you need to run a piece of code only once, there’s a Nuxt composable for that
(since 3.9):
Using callOnce ensures that your code is only executed one time — either on the
server during SSR or on the client when the user navigates to a new page.
It’s only executed one time per route load. It does not return any value, and can be
executed anywhere you can place a composable.
It also has a key similar to useFetch or useAsyncData , to make sure that it can keep
track of what’s been executed and what hasn’t:
By default Nuxt will use the file and line number to automatically generate a unique
key, but this won’t work in all cases.
You may have noticed something weird with useAsyncData and useFetch in Nuxt.
It’s possible to use them either synchronously, or asynchronously, with the await
keyword:
// Synchronously
const { data } = useFetch('some/api');
// Asynchronously
const { data } = await useFetch('some/api');
The trick here is that we can add properties to our Promise . If we return the same
type of object that our Promise will return, we get the same interface of T |
Promise<T> :
function asyncOrSync() {
const asyncOperation = new Promise((resolve) => {
setTimeout(() => {
resolve({
data: "world",
});
}, 1000);
});
return enhancedPromise;
}
Of course, the only benefit to this pattern with useAsyncData is because of reactivity.
We can hook up our reactive values synchronously before the Promise resolves, and
once it does resolve, our values update nicely.
This pattern can be useful with your own custom composables if you aren’t able to
One of the best uses of the useState composable is to sync state across your app, on
the client.
// Component A
const counter = useState('counter', 0);
// Component B
const theSameCounter = useState('counter');
We get back a reactive ref that is now synced across our app!
You can use Pinia or the Data Store Pattern, but if all you need is a single value or two,
this will get you pretty far. No need to make it more complicated!
If you have some really heavy resource that you’d like to load or execute on the client
side without blocking, you can use the onNuxtReady composable:
onNuxtReady(() => {
// Load a big file or something
});
It waits for Nuxt to finish hydrating on the client side and then uses
requestIdleCallback to schedule the task when the browser isn’t doing anything else.
It can be really useful to have access to the instance of the Nuxt app.
One of the most interesting uses is getting access to the hook-based system so we
can extend and modify the runtime behaviour of our Nuxt app:
Now we can do all the usual things, adding components, directives, and plugins.
If you don’t need to use nuxtApp , you can use tryUseNuxtApp instead, which won’t
throw an error if the Nuxt context isn’t available. It will just return null instead.
We can get detailed information on how our page is loading with the
useLoadingIndicator composable:
const {
progress,
isLoading,
} = useLoadingIndicator();
But we have lots of control over how the loading indicator operates:
const {
progress,
isLoading,
start, // Start from 0
set, // Overwrite progress
finish, // Finish and cleanup
clear, // Clean up all timers and reset
error // If we are in an error state
} = useLoadingIndicator({
duration: 1000, // Defaults to 2000
throttle: 300, // Defaults to 200
});
We’re able to specifically set the duration , which is needed so we can calculate the
progress as a percentage. The throttle value controls how quickly the progress
value will update — useful if you have lots of interactions that you want to smooth
out.
The difference between finish and clear is important. While clear resets all
internal timers, it doesn’t reset any values.
File-based routing has some tricky parts around how it handles directories and files
of the same name: ~/pages/foo.vue vs ~/pages/foo/index.vue — which component
does it render?
pages
- foo
- one.vue
- two.vue
- three.vue
- foo.vue // <- this takes precedence
The routes /foo/one , /foo/two , and /foo/three will only render the ~/pages/foo.vue
component. It will need to have a NuxtPage component in order to render the child
components inside the ~/pages/foo/ directory:
<template>
Foo.vue
<NuxtPage />
</template>
If we don’t want child routes at all, we need to get rid of ~/pages/foo.vue and rely only
on the pages inside the ~/pages/foo/ directory.
The best way to understand nested NuxtPage components is to think of them like
nested folders.
pages/
- one.vue
- one/
- two/
- three.vue
// pages/one.vue
<template>
One
<NuxtPage />
</template>
// pages/one/two/three.vue
<template>
Two / Three
</template>
Keep in mind, Nuxt will try to match as many of these URL segments as possible.
Nuxt is able to match the entire route, so it stops. Only one.vue is rendered, and
nothing is rendered from the second, nested, NuxtPage component.
However, if we go to /one/two , we will get a 404. Although we’re able to match the
first part of the route /one against the one.vue component, we run into some issues.
But if we go to the route /one/two/three , we get the page one.vue rendered, with the
page /one/two/three.vue rendered as a child of it! This is because Nuxt can match all
three segments to the three.vue component.
await navigateTo('/dashboard');
When using it in a Vue component, make sure to await the result of the promise it
returns.
It’s also very useful when used in route middleware, allowing you to redirect if needed:
await navigateTo("/another-page", {
redirectCode: 307
});
await navigateTo(
"https://nuxt.com/docs/api/utils/navigate-to#within-route-middleware",
{
external: true,
},
);
Since it wraps Vue Router, we can pass in the same object that router.push accepts
(modified from the Vue Router docs):
// named route with params to let the router build the url
await navigateTo({ name: 'user', params: { username: 'eduardo' } })
Use validate to validate inline whether or not we can actually go to a certain page.
We don’t need route middleware to validate a route. Instead, we can do this inline
using definePageMeta :
definePageMeta({
validate(to) {
if (to.params.id === undefined) {
// Try to match on another route
return false;
}
// Success!
return true;
},
});
This is useful because we might have multiple pages that match a route, and we can
use validate to check the validity of the current route, including params and
parameters.
if (!chapter) {
return createError({
statusCode: 404,
message: 'Chapter not found',
});
}
return true;
},
});
This example is from Mastering Nuxt, where we check if the chapter exists before
trying to render the page for that chapter.
However, validate is technically syntactic sugar for inline middleware. This means we
can’t define both a validate function and define middleware in definePageMeta . We
can refactor our validate function to be an inline middleware with a bit of work:
// ~/pages/index.vue
definePageMeta({
alias: ['/home', '/dashboard'],
})
This page will be rendered when visiting the / , /home , and /dashboard routes.
Unlike a redirect, it will not update the URL or provide a redirect status code. We’re
simply reusing a page for multiple routes.
// ~/pages/admin.vue
definePageMeta({
alias: ['settings'],
})
We can also include parameters in our alias, just like we would with regular pages.
However, we have to use the Vue Router syntax of :param instead of [param] like we
do with file-based routing:
// ~/pages/admin/[id].vue
definePageMeta({
alias: ['settings', '/settings/:id'],
})
You can make sure that Nuxt will scroll to the top of your page on a route change
with the scrollToTop property:
definePageMeta({
scrollToTop: true,
});
If you want more control, you can also use a middleware-style function:
definePageMeta({
scrollToTop: (to, from) => {
// If we came from another docs page, make sure we scroll
if (to.path.includes('docs')) {
return true;
}
return false;
},
});
If you want even more control over the scroll behaviour, you can customize Vue
Router directly using the ~/app/router.options.ts file:
It took me way too long to figure this one out, but here it is:
With the Options API you can use $route and $router to get objects that update
whenever the route changes.
Since Nuxt uses Vue Router internally, this works equally well in Nuxt and vanilla Vue
apps.
The useRoute composable from Vue Router (and included in Nuxt 3) gives us easy
access to the current route:
<template>
<pre>{{ $route }}</pre>
</template>
This route object comes straight from Vue Router, so it contains everything you’d
expect:
• path
• query
• params
• and more
<template>
This page will redirect automatically in {{ timeLeft }}s
</template>
<script setup>
const timeLeft = ref(5);
onMounted(() => {
setInterval(async () => {
if (timeLeft.value === 1) {
await navigateTo("https://michaelnthiessen.com", {
// Redirecting to external URLs is not allowed by default
external: true,
});
}
timeLeft.value = timeLeft.value - 1;
}, 1000);
});
</script>
Once the countdown reaches one second left, it will perform a redirect using
navigateTo with external set to true so we can go to an external URL. Not
something you’ll use often, but nice to have it in your back pocket!
Although, generally, we don’t want to do this because we aren’t able to provide any
status codes. But redirect status codes aren’t always needed!
By default, route rule redirects use the newer 307 temporary status code, but we can
supply our own:
The drawback is that you don’t get any ability to add custom logic. But if all you need
is a simple redirect it’s the best solution.
Redirects through route rules also work on both server-side and client-side, as of Nuxt
3.8 (which added client-side redirects).
Route middleware are run every single time your Vue app changes to a new route.
This is done within Vue itself, but the middleware may run on the server during server-
side rendering.
Each middleware receives a to and from route. They must return one of:
• Nothing — navigation continues normally
• abortNavigation — this is how you’d return an error in order to stop navigation
completely
• navigateTo — if you want to redirect to somewhere else
function(to, from) {
// We can use the `to` and `from` routes to figure
// out what we should be doing in this middleware
if (notValidRoute(to)) {
// Shows a "Page not found" error by default
return abortNavigation();
} else if (useDifferentError(to)) {
// Pass in a custom error
return abortNavigation(
createError({
statusCode: 404,
message: 'The route could not be found :(',
})
);
} else if (shouldRedirect(to)) {
// Redirect back to the home page
return navigateTo('/');
} else {
// If everything looks good, we won't do anything
return;
}
}
// ./middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
// ...
});
To use named middleware on a page, just use the name as a string in definePageMeta :
definePageMeta({
middleware: ['auth'],
});
You can also make global middleware, which will run on all routes. You can make this
happen by adding the .global. suffix to the name: ./middleware/auth.global.ts .
Just be careful with redirects on this one — you don’t want an infinite redirect loop!
Redirecting in route middleware is one of the best ways to encapsulate more complex
redirection logic, since it will run on both the server and client:
By default, navigateTo will use a 302 redirect code (temporary redirect). We can
change this status code by using the redirectCode property in the options object:
Another cool thing — with route middleware can intercept routes that don’t exist
without throwing errors (unlike server routes/middleware).
The default slot on the NuxtPage component is passed all the route props, so we can
have more control if we need it:
We can use it just like we’d use the RouterView component from Vue Router (say that
five times fast!).
const projectId = 1
const { data: tracks, pending, error } = useFetch(
`https://api.example.com/projects/${projectId}/tracks`
)
<template>
<div>
<h1>Project Tracks</h1>
<ul v-if="!pending && !error">
<li v-for="track in tracks" :key="track.id">
{{ track.name }}
</li>
</ul>
<p v-if="pending">Loading...</p>
<p v-if="error">Error: {{ error.message }}</p>
</div>
</template>
In the example above, we're checking if pending is true and displaying a loading
message if so. We're also checking if there's an error and displaying the error
message if one occurs.
To make sure that your component updates when the project ID changes, you can
pass in the projectId as a ref instead:
Don’t use useFetch for one-off requests based on user actions though. It’s best used
for GET requests that are set up once when the component loads.
If you want, you can check out the documentation for more information.
It synchronizes the state between our server and client, so during SSR it will only fetch
the data on the server.
Here's an example of how you might use useAsyncData in a music production app to
fetch a list of instruments:
<template>
<div>
<h1>Available Instruments</h1>
<ul v-if="status === 'success'">
<li v-for="instrument in instruments" :key="instrument.id">
{{ instrument.name }}
</li>
</ul>
<p v-if="status === 'pending'">Loading...</p>
<p v-if="status === 'error'">Error: {{ error.message }}</p>
</div>
</template>
In this example, we're using useAsyncData to fetch the list of instruments and assign
the result to a reactive instruments variable.
We also have access to status and error properties, which can be used to display
loading and error states in our template.
Since 3.9 we can control how Nuxt deduplicates fetches with the dedupe parameter:
useFetch('/api/search/', {
query: {
search,
},
dedupe: 'cancel' // Cancel the previous request and make a new request
});
However, you can change this behaviour to instead defer to the existing request
— while there is a pending request, no new requests will be made:
useFetch('/api/search/', {
query: {
search,
},
dedupe: 'defer' // Keep the pending request and don't initiate a new one
});
This gives us greater control over how our data is loaded and requests are made.
Internally, the key parameter is used to create a unique identifier for the fetched
data, so Nuxt knows if the data has already been fetched during the server render or
not.
When the data is fetched on the server and passed along with the client bundle, the
client knows it doesn’t need to re-fetch that data since it’s already been fetched.
If you don’t provide a key, Nuxt will automatically create one for you based on the line
and file of where it’s used. However, it’s better to provide your own key, as this is more
reliable.
One downside of using Lazy* components is that they are only downloaded right
when they are needed:
The biggest benefit of these Lazy* components is when they’re big, but that also
means it takes longer to download the component and render to the page.
Instead, we can prefetch any global component at a time of our own choosing, using
prefetchComponents :
await prefetchComponents('Counter');
Here’s an example where we’re doing both. If we click “Fetch Counter” we can preload
the component, so it’s already downloaded when the v-if is true :
<!-- Must use Lazy prefix or component will be loaded on page load -->
<LazyCounter v-if="showCounter" />
</div>
</template>
<script setup>
const showCounter = ref(false);
const counterFetched = ref(false);
If we don’t prefetch, the component will fall back to regular Lazy* behaviour and
load as soon as the v-if becomes true .
Note: Even though the Counter component here is global (and async by default), you
still have to use the Lazy* prefix or it will be included in the initial page render.
In Nuxt, the concept of client side and server side execution gets a bit confusing due
to the universal rendering (server-side rendering combined with client-side
rendering).
Sometimes code is run on both the server and the client. Other times, only the client.
Yet other times, only on the server.
The easiest to remember is that Nitro is our server framework, so any code here only
runs on the server. This is everything inside of the ~/server directory, including routes,
middleware, and plugins.
Here, we have access to the event object that represents the request and response
that we’ll eventually give.
Our Vue app can be executed on both the server and client.
if (!import.meta.server) {
// Don't run this on the server
}
if (!import.meta.client) {
// Don't run this on the client
}
In your components though, there’s a good chance you want to use onMounted to run
client-only code:
onMounted(() => {
// Code that only runs once the Vue component is
// mounted on the client
});
One use of useState is to manage state between the server-side and the client-side.
By default, Vue doesn’t do any hydration. If we’re using ref or reactive to store our
state, then during the server-side render they don’t get saved and passed along.
When the client is loading all our logic must run again.
But useState allows us to correctly sync state so we can reuse the work that was
already done on the server.
If multiple users all visit your website at the same time, and those pages are all first
rendered on the server, that means that the same code is rendering out pages for
different people at the same time.
This creates the opportunity for the state from one request to accidentally get mixed
up in another request.
But useState takes care of this for us by creating an entirely new state object for
each request, in order to prevent this from happening. Pinia does this too, which is
why it’s also a recommended state management solution.
If you have an error during your server-side render, you can use the
<NuxtClientFallback> component to render some fallback content:
<template>
<NuxtClientFallback>
<ServerComponentWithError />
<template #fallback>
<p>Whoops, didn't render properly!</p>
</template>
</NuxtClientFallback>
</template>
But we want this to be stable through SSR so we don’t get any hydration errors.
And while we’re at it, why don’t we make it a directive so we can easily add it to any
element we want?
const directive = {
getSSRProps() {
return { id: generateID() };
},
}
When using it with Nuxt, we need to create a plugin so we can register the custom
directive:
// ~/plugins/dynamic-id.ts
const generateID = () => Math.floor(Math.random() * 1000);
In Nuxt 3.10+, you can also use the useId composable instead:
<script setup>
const id = useId();
</script>
Normally, custom directives are ignored by Vue during SSR because they typically are
there to manipulate the DOM. Since SSR only renders the initial DOM state, there’s no
need to run them, so they’re skipped.
But there are some cases where we actually need the directives to be run on the
server, such as with our dynamic ID directive.
It’s a special function on our directives that is only called during SSR, and the object
returned from it is applied directly to the element, with each property becoming a
new attribute of the element:
getSSRProps(binding, vnode) {
// ...
return {
attribute,
anotherAttribute,
};
}
We can access the entire payload sent from the server to the client through
useNuxtApp :
All of the data that’s fetched from our data-fetching composables is stored in the
data key:
The key that is used is based on the key param passed in to the composable. By
default, it will auto-inject the key based on the filename and line number, but you can
pass in a custom value if needed.
Similarly, all the data that’s stored for useState is in the state key, and can be
accessed in the same way.
We also have a serverRendered boolean that let’s us know if the response was server
rendered or not.
We use defineEventHandler to create an event handler, and then directly export that
so that Nuxt can use it.
All of our server route files are placed in the /server/routes directory. Routing here
works exactly like page routing — it’s based on the filename. So if we put our server
route in /server/routes/hello.ts , we can access it by sending a request to /hello .
However, Nuxt also gives us a shorthand for API routes since these are the most
common type.
Any route placed in /server/api will automatically be prefixed with /api . If we place
our event handler in /server/api/hello.ts , we can access it by sending a request to
/api/hello instead.
One of my favourite features is how h3 — the server that Nuxt uses internally
— handles return values from defineEventHandler .
Instead of needing to properly set content-type headers and format our Response
object correctly, we just return whatever we need to return:
If we just need to send a status code, just return the status code:
All of these can be done async, either by wrapping in a Promise or using async/await .
But don’t return errors. Instead, it’s recommended to throw them using createError
instead:
There are a couple main ways we can get inputs or arguments passed into an event
handler:
1. Route parameters
2. Query parameters
3. From the body of the request object
(We can also use cookies and headers and other values in some cases).
We can use a query parameter instead if we change our route to this structure:
/server/api/icecream?flavor=chocolate
Refactoring to use the getQuery method and renaming our file to /server/api/
icecream.ts gives us this:
We can also rewrite this handler to get the flavor from the body of the request using
the readBody utility from h3:
If we send a request with a body of { 'flavor': 'chocolate' } we’ll see it return the
chocolate flavor back to us!
Grabbing data from the body of a request can be done using the readBody method
from h3 :
If we just want the raw string value, we can use readRawBody instead:
We could also use a library like zod for more powerful parsing:
Getting values out of the query parameter in our server routes is straightforward:
{
hello: 'world',
flavours: [
'chocolate',
'vanilla',
},
}
We sometimes need to check the request headers on the server (in Nitro) for
authentication, security, caching, or rate limiting, and more.
This is an abstraction of useRequestHeaders , since there are a lot of times where you
need just one header. But you can also use that one to grab them all:
We can prevent our server routes from being run on the wrong HTTP method by
specifying the correct one in the file name, like ~/server/api/users.get.ts .
If we try to hit the /api/users endpoint with a POST request, we’ll get back a 405
Method Not Allowed error.
This let’s us split up logic for different methods into different files, instead of having
all the logic in a single file.
But if we want it all in one file, we can do a check to see which method we’re dealing
with by looking at event.method :
If this endpoint is called with anything other than a GET request, it’ll return a 405
immediately.
Server middleware are functions that are run on every request before our server
routes handle them. This makes them great for inspecting or modifying the request in
some way:
// ~/server/middleware/auth.ts
event.context.user = user;
event.context.authenticated = user.isAuthenticated;
});
The context property on the event object is there for us to add to, so we can provide
additional context if needed.
// ~/server/api/user.get.ts
// ...
});
You’ll notice that both middleware and server routes use defineEventHandler . The only
real difference with middleware is that they are executed on every request, and
always executed before the server route itself.
To create a redirect in a Nuxt server route, we can use the sendRedirect method from
h3 :
By default, it will use a 302 (temporary redirect) status code. But we can make it use
a 307 or any other status code if we prefer:
Creating redirects in server middleware works exactly the same as with server routes
— we’re still in the Nitro and h3 server. The main difference is that server middleware
are run on every request we get, so they’re effectively global. This means we just need
to be more careful with our business logic.
The actual route handler for /server-middleware won’t get executed, but it does need
to exist or the router will give us an error. If we instead did this in a route middleware,
the actual route doesn’t need to exist since the redirect happens inside the router
itself.
Nuxt offers a unique solution to the limitations of SPAs and SSRs by combining their
strengths. This approach, called Universal Rendering, provides the best of both
worlds.
On the first page load, Nuxt uses SSR to deliver a fast initial experience.
It processes the request on the server and sends back the HTML and other necessary
files, similar to a traditional SSR app. This ensures that users are not kept waiting,
which is particularly important for maintaining user engagement and optimizing
search engine rankings.
It also loads the entire app as an SPA, so everything after the first page load is
extremely quick. Once the initial page is loaded, Nuxt switches to SPA mode, allowing
users to navigate within the app without needing to make round trips to the server.
It's worth noting that Nuxt goes beyond simply combining SSR and SPA approaches.
The framework also includes numerous performance enhancements and
optimizations under the hood.
For example, Nuxt ensures that only necessary data is sent, and it intelligently
prefetches data right before it's needed.
These optimizations, along with many others, contribute to the overall speed and
efficiency of a Nuxt application. In essence, Nuxt provides a seamless user
experience without sacrificing performance, offering developers the best of both SPA
Sometimes we’ll need to make sure to pass cookies in to our server routes:
We often need to do this when we use authentication on our server routes, since the
user’s session is often stored in a cookie. On the client side this works fine, as cookies
are included with each request. But during server-side rendering the cookie isn’t
passed around.
If we don’t pass that cookie along though, our server route doesn’t know the user is
logged in and blocks the request.
We can solve this issue by always passing along the cookie using the
useRequestHeaders composable and modifying how we use useFetch or useAsyncData .
Now, during SSR, useRequestHeaders will pass the cookie along with the request, so we
can successfully access the protected endpoint.
If we wanted to make a basic link shortener service, we can do that pretty easily
using a server route.
// Mock database
// We would normally get this data from a database somewhere
const db = {
h4sh: "https://www.nuxt.com",
"an0th3r-h4sh": "https://michaelnthiessen.com",
};
We’ll set up a mock database for simplicity, and then look up the correct URL to
redirect to based on the hash parameter in the route. We tell the router that we want
to get a route param by using square brackets in the filename, that’s what the [hash]
part is for.
To get this value from the URL, we use the getRouterParam method from h3 . Then, we
can use that to lookup what the redirect URL should be. Here we use a plain old
Javascript object for simplicity.
After that, we use the sendRedirect method from h3 to redirect the client using a
307 temporary redirect.
Nitro, the server that Nuxt uses, comes with a very powerful key-value storage
system:
// Save a value
await storage.setItem('some:key', value);
// Retrieve a value
const item = await storage.getItem('some:key');
It’s not a replacement for a robust database, but it’s perfect for temporary data or a
caching layer.
One great application of this “session storage” is using it during an OAuth flow.
In the first step of the flow, we receive a state and a codeVerifier . In the second
step, we receive a code along with the state again, which let’s us use the
codeVerifier to verify that the code is authentic.
We need to store the codeVerifier in between these steps, but only for a few
minutes — perfect for Nitro’s storage!
// ~/server/api/oauth
// ...
const storage = useStorage();
const key = `verifier:${state}`;
await storage.setItem(key, codeVerifier);
// ...
// ...
const storage = useStorage();
const key = `verifier:${state}`;
const codeVerifier = await storage.getItem(key);
// ...
A simple and easy solution, with no need to add a new table to our database and
deal with an extra migration.
This just scratches the surface. Learn more about the unstorage package that
powers this: https://github.com/unjs/unstorage
// Part of my blog
import BasicLayout from './BasicLayout.vue';
import Footer from '../components/Footer';
import Subscribe from '../components/Subscribe';
import LandingMat from '../components/LandingMat';
import Logo from '../icons/Logo';
import LogoClip from '../icons/LogoClip';
import TriangleShape from '../icons/TriangleShape';
import SquareShape from '../icons/SquareShape';
Just use your components, composables, or layouts where you need them, and Nuxt
takes care of the rest.
It may seem like a small thing, but auto-imports in Nuxt make the whole developer
experience so much nicer. It only imports what you need, when you need it.
Yes, your dependencies are now less explicit. But if you keep your components and
composables small enough it shouldn’t matter that much. You should still be able to
see pretty quickly what’s going on in your application.
You can completely disable auto-imports by using the new imports.scan option:
This will ignore any directories listed in the imports.dirs option, as well as ignoring
auto-imports for the ~/composables and ~/utils directories.
This will make the imports in your project more explicit, so it’s clearer where things are
coming from. Some developers prefer this because it adds to the readability of the
code.
We often need different config based on the environment our app is running in,
whether we’re running tests, in dev mode, or running in prod.
We can use the $test , $development and $production keys for environment-specific
configs.
You can specify composables to be tree shaken out of the client bundle (or server
bundle):
// nuxt.config.ts
optimization: {
treeShake: {
composables: {
client: {
'package-name': ['useSomeUnnecessaryComposable'],
}
},
},
},
By default, Nuxt tree shakes composables from Vue that don’t work on the server or
client (many work in both places) and composables built-in to Nuxt.
For example, if you never use the useId composable, it won’t be included with your
app bundle, so it will load just that much faster.
Using the same syntax as your .gitignore file, you can configure Nuxt to ignore files
and directories during build time:
You can include CSS files in every single page using the css config value in
nuxt.config.ts :
It will also use the right pre-processor, but you have to make sure that it’s installed:
This is handy for applying consistent styling, like including your own utility classes
(like Tailwind does), or CSS resets so each page has a clean starting point.
The app.config is used to expose public variables that can be determined at build
time, such as theme variants, titles, or other non-sensitive project configurations.
These values are set in the app.config.ts file.
To define app.config variables, you need to create the app.config.ts file in the root of
your project:
// app.config.ts
To access app.config values within your application, you can use the useAppConfig
composable:
Although the appConfig type is automatically inferred, you can manually type your
app.config using TypeScript if you really need to. This example is from the docs:
// index.d.ts
declare module 'nuxt/schema' {
interface AppConfig {
// This will entirely replace the existing inferred `theme` property
theme: {
// You might want to type this value to add more specific types
// than Nuxt can infer, such as string literal types
primaryColor?: 'red' | 'blue'
}
}
}
The runtimeConfig is used to expose environment variables and private tokens within
your application, such as API keys or other sensitive information. These values can be
set in the nuxt.config.ts file and can be overridden using environment variables.
To set up private and public keys in your nuxt.config.ts file, you can use the following
code example:
You can set environment variables in a .env file to make them accessible during
development and build/generate.
Just make sure that you use the right prefixes. Put NUXT_ before everything so that
Nuxt will know to import it, and don’t forget to add in PUBLIC if it’s a value in the
public field of your config:
NUXT_PUBLIC_BOOK_STORE_API_BASE_URL = "https://api.bookstore.com"
NUXT_BOOK_STORE_API_SECRET = "super-secret-key"
Both runtimeConfig and app.config allow you to expose variables to your application.
However, there are some key differences:
1. runtimeConfig supports environment variables, whereas app.config does not.
This makes runtimeConfig more suitable for values that need to be specified
after the build using environment variables.
2. runtimeConfig values are hydrated on the client side during run-time, while
app.config values are bundled during the build process.
3. app.config supports Hot Module Replacement (HMR), which means you can
update the configuration without a full page reload during development.
4. app.config values can be fully typed with TypeScript, whereas runtimeConfig
cannot.
Any files that you put into the ~/public directory get included directly in the build
output of your Nuxt app. They are not modified in any way.
This public directory is served at the root of your app. This means that if your
~/public folder looks like this:
public/
- cats.png
- songs/
- dubstep.wav
- chopin.mp3
yourwebsite.com/cats.png
yourwebsite.com/songs/dubstep.wav
yourwebsite.com/songs/chopin.mp3
Remember, these files are not touched at all. The contents of ~/public are copied
straight into your dist folder.
• favicon
• sitemaps
All of the code in your application is processed by a bundler. Nuxt uses Vite by
default.
The bundler will start at app.vue and pull in all of its dependencies, then all of their
dependencies, and so on, until all of the code in your app has been processed.
This becomes the bundled output that you can use to deploy your application.
If you import something other than “code” into a component, the bundler still has to
include those files as well. Otherwise, they wouldn’t be deployed alongside all of your
other code:
import '~/assets/styles/radStyleSheet.css';
It doesn’t actually matter where you put these files. But we have to put all of these
other assets somewhere to keep things organized, so we typically use ~/assets by
convention.
Most things usually end up being processed by the bundler, so you’ll commonly find
these in an ~/assets folder:
• Stylesheets
• Icons
• Fonts
Images can also go in there if you need to process them with a Vite plugin. But it’s
better to keep them in /public and use NuxtImg to process and cache them instead.
In any web app, including with Nuxt, we have two main ways to deal with our non-
code assets:
1. We can process them through our bundler using the /assets directory (or
importing from somewhere else)
2. We can leave them as is inside the /public directory
There are three questions I use to help me make this decision. If the answer is yes to
any, put it through your bundler.
If you have a bunch of images, you likely want to make sure those images are
compressed and optimized before shipping them with your web application.
A sitemap, on the other hand, is good as it is. No need to do anything extra with it.
Our code changes often, so we want to include it in our bundle. This is because our
bundler will give our code bundle a unique name every time it changes, based on a
hash of the contents.
This means the browser will be forced to always have the latest version of our code.
Page crawlers will be looking for a file specifically called robots.txt , so we need to
preserve that filename.
For most other assets though, the specific filename doesn’t matter so much, so we
They let us break down, and then combine together, smaller Nuxt apps so we can
better organize, share, and modularize our code.
Unlike a more typical npm package, these layers benefit from all of the Nuxt magic:
auto-imports, file-based routing, Nuxt configs, and more.
// layer/nuxt.config.ts
export default defineNuxtConfig({});
If we have two sublayers, blue and green , these layers can use components (and
other files) from each other without extending each other:
// Blue.vue
<template>
Use a component from the Green layer.
<GreenButton />
</template>
This is because they are both imported by the “main” app or top layer.
When Blue.vue is being rendered, the GreenButton exists because it was also
imported from the sublayer, so it all works.
This is useful if you’re splitting your app into layers for organizational purposes.
One issue with importing all sorts of components, layouts, and other things from
layers, is naming collisions. If we have a Button component in each layer, that leads
to some conflicts.
Because of how layers work, this also applies within your sub layers if they use a
component that has a naming collision.
Let’s say that we have a Button component defined in both the blue and green
layers, as well as in our current layer:
- components
- Button.vue
- green
- components
- Button.vue
- blue
- components
- Button.vue
<template>
Please click this button:
<Button>Click me!</Button>
</template>
This won’t render the Button from the blue layer. Component precedence doesn’t
change regardless of which layer you’re in, so it will still render the components/
Button.vue because the “top” layer takes precedence.
<template>
Please click this button:
<BlueButton>Click me!</BlueButton>
</template>
<script setup>
import BlueButton from '~/components/Button.vue';
</script>
Pages from sub layers create routes just as they would if we were using the layer as
its own app. It doesn’t prepend any path or anything like that:
- pages
- index.vue /
- nested.vue /nested
- blue
- pages
- blue.vue /blue
- green
- pages
- green.vue /green
But this gives us some interesting opportunities, since all pages are treated equally.
We can have layers provide child routes (or parent routes, it doesn’t matter), and use
them like any other routes:
- pages
- index.vue /
- nested.vue /nested
- blue
- pages
- nested
- blue.vue /nested/blue
- green
- pages
- nested
- green.vue /nested/green
We need to make sure that our nested.vue component renders a child route using
NuxtPage :
Now, if we navigate to /nested/blue we’ll render out nested.vue as the parent, and
blue.vue as the child component.
There are lots of configuration options available in Nuxt. In fact, your nuxt.config.ts
might start getting really long and hard to read!
But with layers, not only is our code separated, but our config is as well.
Our blog layer can use the Nuxt Content module, but leave it out of the main app:
We can define a more focused runtimeConfig in each layer, keeping things simpler.
What if we want a layer that has pages and layouts, but we want the components to
be private and not available to the main app?
// privateLayer/nuxt.config.ts
export default defineNuxtConfig({
components: [],
});
There are lots of modules for Nuxt, and there is a useful naming convention that helps
to understand what kind of module you’re dealing with.
• Official modules — these are always prefixed with @nuxt/ and are actively
maintained by the Nuxt team. For example, @nuxt/image , @nuxt/content , and
@nuxt/fonts .
You can find a searchable list of official and community modules on the Nuxt website.
If you’ve created a module and want it included, you can open a PR on the Nuxt
Modules Github repo. You can also find all the other community modules there if you
want to help with maintaining one of them!
The most powerful way to encapsulate custom Nuxt functionality is through modules.
You don’t have to share them either, you can create local modules just for your app
inside of ~/modules :
// ~/modules/myModule.ts
The files inside of ~/modules are auto-imported one-level deep, so we don’t need to do
anything more.
However, it’s best to use the defineNuxtModule wrapper, because it adds a bunch of
extra stuff to make the modules work better, like specifying compatibility.
Even when just developing our own module, we get some benefits, like better type
hints and declarative hooks:
Modules are best when we need to modify build behaviour of our app, or add runtime
files and assets during build. We can also do a lot of this stuff through layers, but
without programmatic flexibility.
Modules can only affect your Nuxt app during build time. However, we can use a
module to change the runtime behaviour by changing our app at build time.
import {
defineNuxtModule,
createResolver,
addComponentsDir,
} from '@nuxt/kit';
You can add pages using extendPages , or update routing using extendRouteRules or
addRouteMiddleware .
In fact, there are methods in @nuxt/kit that let you modify almost any part of your
Nuxt app at build time.
We get access to a special set of build-time and development time hooks in our
modules, which let us modify how Nuxt works:
For example, using the builder:watch hook we can track all the updates being made
to the project during development. Every time a file in our Nuxt project is created/
updated/deleted, this hook is called.
We can modify the runtime behaviour of our Nuxt app by creating plugins inside the
~/plugins/ directory:
We can also use an object syntax that gives us more flexibility and expressivity:
For example, instead of defining hooks in the setup , we can define them
declaratively:
hooks: {
// Define the hook declaratively
'vue:error'(err) {
// Handle the Vue error
console.error(err);
},
});
});
When using the object syntax, Nuxt will statically analyze and pre-load the plugins.
This lets it optimize the build, and means that you don’t need to worry about plugin
ordering when defining hooks in this way.
Nuxt comes with a lot of different hooks, but I’ve found myself quite confused about
what hooks are available when, and where we can use them.
We can also access server hooks in our modules by using the nitro:init hook to wait
for nitro to be initialized.
You can write custom plugins to modify Nitro’s runtime behaviour. They live inside of
~/server/plugins :
// ~/server/plugins/handleError.js
You have access to the nitroApp , which lets you hook into specific runtime hooks
that Nitro provides:
• close — when Nitro is closing down
• error — when an error is encountered
• render:response — after Nitro has finished server rendering a page
• request — right after a new request has been received
• beforeResponse — right before sending a response
• afterResponse — right after sending a response
Nuxt also defines some additional hooks on the Nitro app we can use:
• dev:ssr-logs — contains all the server-side logs generated from that request
• render:html — right before Nuxt server renders the page
• render:island — right before Nuxt server renders an island component
There are two types of hooks available on the server: server hooks that come from
Nitro, and server hooks that Nuxt adds to Nitro.
(This is why the list of hooks in the Nuxt and Nitro docs are different in case you were
wondering).
We can hook directly into Nitro’s behaviour by using hooks inside of Nitro plugins:
Here’s the full list of Nitro hooks and the list of Nitro hooks available in Nuxt (including
ones added by Nuxt).
You can set up a plugin to load in parallel, so it doesn’t block the loading of other
plugins:
This will immediately start loading and executing the next plugin in the plugin list,
while this one loads asynchronously. Here is some pseudo code that explains how this
works:
Normally, plugins are initialized sequentially — based on the order they are in the
filesystem:
plugins/
- 01.firstPlugin.ts // Use numbers to force non-alphabetical order
- 02.anotherPlugin.ts
- thirdPlugin.ts
But we can also have them loaded in parallel, which speeds things up if they don’t
depend on each other:
However, sometimes we have other plugins that depend on these parallel plugins. By
using the dependsOn key, we can let Nuxt know which plugins we need to wait for, even
if they’re being run in parallel:
You can split up plugin logic based on client-side or server-side by using the correct
suffixes:
• Client-side: ~/plugins/*.client.ts
• Server-side: ~/plugins/*.server.ts
You can also set this in nuxt.config.ts by setting the src and mode when listing your
plugins:
plugins: [
'@nuxt/content',
{
src: '~/plugins/some-plugin.ts',
mode: 'client',
},
]
Remember, any plugins inside of ~/plugins will be auto-registered and loaded after
the plugins listed here in the config. They will default to being isomorphic, running on
both client and server side.
Nuxt uses the NuxtIsland component internally for server components and pages, but
you can use it directly if you need to:
<NuxtIsland
name="Counter"
:props="{
startingCount,
}"
/>
The name is the same name you’d use in a template to use an auto-imported
component (the component needs to be global ). This means a component at
~/components/server/Counter.server.vue would be used like this with the filename
prepended:
<NuxtIsland
name="ServerCounter"
:props="{
startingCount,
}"
/>
This component must be a server component though! The easiest way to do this is by
putting the *.server.vue suffix on it.
Whenever the props of this component change on the client, a new request will be
made to the server to re-render the component. You can also force this behaviour by
using the refresh method on the ref :
<script setup>
const serverCounter = ref(null);
However, keep in mind that each NuxtIsland component is rendered as a full Nuxt app
on the server. So having multiple of these on a single page can lead to performance
issues.
// Parent.server.vue
<template>
<div>
<div>This is on the server</div>
Because server pages are also server components, you must use this attribute on
server pages as well.
You can write more complex components that have different logic on the server and
the client by splitting them into paired server components. All you need to do is keep
the same name, changing only the suffix to *.client.vue and *.server.vue .
<template>
<div>This is paired: {{ startingCount }}</div>
</template>
We grab our startingCount prop and render it to the page — no need to do anything
else because we’re not interactive at this point.
Then, Nuxt will find Counter.client.vue and ship that to the client in order to hydrate
and make the component interactive:
onMounted(() => {
setInterval(() => {
offset.value++;
}, 1000);
});
</script>
We’re careful to make sure we avoid hydration mismatches, and we bootstrap our
interactivity.
You can put interactive components into the slot of server components, as long as
the parent component is also interactive:
// Parent.client.vue
<template>
<ServerComponent>
<!-- We don't need nuxt-client attr here -->
<Counter :starting-count="39" />
</ServerComponent>
</template>
This works because of the way that slots decouple the Vue component tree from the
tree rendered to the DOM. Here, Counter is a child of Parent in Vue tree, although
when rendered it becomes a child of the ServerComponent :
This means that it behaves normally, and the ServerComponent doesn’t really affect
how it is rendered.
However, if the slot content is being rendered in the context of a server component, it
won’t become interactive:
Because the slot is being rendered in a server component context, it will not become
interactive on the client-side.
// Parent.client.vue
<template>
<ServerComponent>
<ServerComponent>
<Counter :starting-count="39" />
</ServerComponent>
</ServerComponent>
</template>
In fact, it appears that slots within server components only work one level deep,
regardless of what’s in the slot.
However, there are no restrictions on putting server components into other slots as
content themselves.
If there’s an error with rendering your server component, there’s a #fallback slot that
will be rendered on the client-side:
<NuxtIsland
name="ServerCounter"
:props="{
startingCount,
}"
>
<template #fallback>
Counting failed!
</template>
</NuxtIsland>
We can easily set a page in our /pages folder to be either client-side or server-side
rendered only, just by changing the suffix.
The client-page.client.vue will only render on the client-side. The exception is for any
server components that you have in there. Those will be rendered on the server:
<template>
<div>Client page: {{ count }}</div>
onMounted(() => {
setInterval(() => {
count.value++;
}, 1000);
});
</script>
onMounted(() => {
setInterval(() => {
count.value++;
}, 1000);
});
</script>
If you navigate to /server-page you’ll get “Server page: 6”, and that’s it. No counting,
since the Javascript is never shipped.
Remember, server components are required to have a single root node, and a server
page is really just a server component:
<template>
<div>
<div>Server page.</div>
<div>With multiple root nodes.</div>
</div>
</template>
But unlike a regular server component, you cannot nest interactive components
within it, unless you have set experimental.componentIsland.selectiveClient in your
config to deep :
<template>
<div>
<div>Server page: {{ count }}</div>
<!-- Renders "17" but doesn't become interactive by default -->
<Counter nuxt-client :starting-count="17" />
</div>
</template>
Hydration errors are one of the trickiest parts about SSR — especially when they only
happen in production.
If you aren’t using Nuxt, you can enable this using the new compile-time flag:
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__ . This is what Nuxt uses.
Enabling flags is different based on what build tool you’re using, but if you’re using
Vite this is what it looks like in your vite.config.js file:
Turning this on will increase your bundle size, but it’s really useful for tracking down
those pesky hydration errors.
To create an error, we’ll throw a Nuxt error that’s returned by the createError method:
throw createError({
statusCode: 500,
statusMessage: 'Something bad happened on the server',
});
You’ll also use it inside your Nitro event handlers in your server routes (your API):
return lesson;
});
(I wrote a whole series on using Prisma with Nuxt and Supabase if you want to learn
more about that.)
Here, we’re throwing an error using createError when the user is not defined. This
prevents the request from continuing, and immediately returns a 401 error back to
whoever is calling this endpoint.
To create an error page, we need to add an error.vue file to the root of our
application, alongside our app.vue and nuxt.config.ts files.
This is important, because this server page is not a “page” that is seen by the router.
Not all Nuxt apps use file-based routing or use Vue Router, so we can’t rely on Vue
Router to display the error page for us.
<template>
<NuxtLayout>
<div>
<h1>Dang</h1>
<p>It looks like something broke.</p>
<p>Sorry about that.</p>
</div>
</NuxtLayout>
</template>
Not super helpful, but we can make it better by using the useError composable to
grab more details about the global error:
We can update our error page to include a check for this statusCode :
<template>
<NuxtLayout>
<div class="prose">
<template v-if="error.statusCode === 404">
<h1>404!</h1>
<p>Sorry, that page doesn't exist.</p>
</template>
<template v-else>
<h1>Dang</h1>
<p>
<strong>{{ error.message }}</strong>
</p>
<p>It looks like something broke.</p>
<p>Sorry about that.</p>
</template>
<p>
Go back to your
<a @click="handleError">
dashboard.
</a>
</p>
</div>
</NuxtLayout>
</template>
Using this strategy we’re able to render what we need based on the type of error and
the message that the error contains.
We can use the built-in NuxtErrorBoundary component to contain errors in our Nuxt
apps:
<NuxtErrorBoundary>
<!-- Put components in here -->
</NuxtErrorBoundary>
This lets us intelligently handle and contain errors. Without this boundary, any errors
will bubble up until they’re caught, potentially crashing your entire app.
We can provide a relevant error UI based on what’s inside our default slot:
<NuxtErrorBoundary>
<UserLogin />
<template #error="{ error }">
<div>
<p>Unable to login! Try again later.</p>
<p>{{ error.message }}</p>
</div>
</template>
</NuxtErrorBoundary>
We can reset this error and re-render the default slot by setting the error back to
null :
We can reset the error by setting the error ref back to null :
<NuxtErrorBoundary>
<UserLogin />
<template #error="{ error }">
<div>
<p>Unable to login! Try again later.</p>
<p>{{ error.message }}</p>
<button @click="recoverFromError(error)">
Reset
</button>
</div>
</template>
</NuxtErrorBoundary>
Well, the main benefit of an error boundary is that we can contain errors within our
application, and then handle them in specific ways instead of just throwing up a
generic error page.
For example:
• NuxtPage components that represent nested routes
• Widgets on a dashboard
• Modals
The most basic thing we can do is reset the error and then navigate somewhere else
using the clearError method:
<template>
<NuxtLayout>
<div class="prose">
<h1>Dang</h1>
<p>
<strong>{{ error.message }}</strong>
</p>
<p>It looks like something broke.</p>
<p>Sorry about that.</p>
<p>
Go back to your
<a @click="handleError">
dashboard.
</a>
</p>
</div>
</NuxtLayout>
</template>
It’s important to understand this distinction because these errors happen under
different circumstances and need to be handled differently.
Global errors can happen any time the server is executing code. Mainly, this is during
an API call, during a server-side render, or any of the code that glues these two
together.
Client-side errors mainly happen while interacting within an app, but they can also
happen on route changes because of how Nuxt’s Universal Rendering works.
It’s important to note that the NuxtErrorBoundary component only deals with client-
side errors, and does nothing to handle or clear these global errors.
To do that, we need to use the error handling composables and utilities from Nuxt:
• useError
• clearError
• createError
• showError
Dealing with errors properly in route middleware is (thankfully!) not that different
from what we’ve already seen.
if (notValidRoute(to)) {
// Shows a "Page not found" error by default
return abortNavigation();
} else if (useDifferentError(to)) {
// Pass in a custom error
// 👇We need to wrap createError with abortNavigation
return abortNavigation(
createError({
statusCode: 404,
message: 'The route could not be found :(',
})
);
} else if (shouldRedirect(to)) {
// Redirect back to the home page
return navigateTo('/');
} else {
// If everything looks good, we won't do anything
return;
}
});
In route middleware we can’t simply throw an error, we have to pass it to the special
abortNavigation method, and then return the Promise from that method instead:
Returning abortNavigation will stop the navigation that is currently taking place, and
instead redirect to an error page.
For your unit tests, @nuxt/test-utils lets you opt-in to a Nuxt environment by adding
.nuxt. to the filename of your test:
./tests/MyComponent.nuxt.test.ts
You can also add a special comment at the top of the file:
@vitest-environment nuxt
// vitest.config.ts
import { defineVitestConfig } from '@nuxt/test-utils/config';
When writing unit tests, you have access to a bunch of helper methods.
One super useful one is mountSuspended . It lets you mount any component inside your
Nuxt context with async setup:
describe('MyComponent', () => {
it('renders the message correctly', async () => {
const wrapper = await mountSuspended(MyComponent);
expect(wrapper.text()).toContain('This component is set up.');
});
});
You’re also able to mount your app at a specific route, by passing in the App
component and a route:
describe('About', () => {
it('renders the about page', async () => {
const wrapper = await mountSuspended(App, { route: '/about' });
expect(wrapper.text()).toContain('Hi, my name is Michael!');
});
});
It's a convenience method to make it easier to mock anything that Nuxt would
normally auto-import:
mockNuxtImport('useAsyncData', () => {
return () => {
return { data: 'Mocked data' };
};
});
// ...tests
This is actually a macro that gets transformed into vi.mock , so it can only be used
once per file.
When testing, you'll often need to shallow render a component — mocking out any
descendent components to keep your test simpler.
With @nuxt/test-utils you can use the mockComponent utility method to help with that:
// ...tests
If you've ever written unit tests, you'll have needed to mock out API endpoints that
are used in your components or stores.
With @nuxt/test-utils this is really simple, because you get the registerEndpoint
utility method:
// ...tests
You can mock any server route (API endpoint), including external endpoints if you
need.
It’s really easy to customize how our Markdown is rendered in Nuxt Content by writing
custom prose components.
If we wanted to add a filename to the code block component, we’d copy over
ProsePre and update it a little:
<style>
We use some conditional classes to adjust the border rounding. When the filename is
set, we'll need to make the top of the code block square so we don't have any weird
gaps.
You may notice the formatting of the pre and slot tags are funny — this is actually
necessary. Because the pre tag preserves all whitespace, if we formatted it with
newlines those newlines would end up rendered to our page as well.
• Newsletters: content/newsletters/
By default though, Nuxt Content would set up these routes to include those prefixes.
But I want all of my routes to be at the root level:
• Articles: michaelnthiessen.com/my-latest-article
• Newsletters: michaelnthiessen.com/most-recent-newsletter
We can do this manually for each Markdown file by overriding the _path property
through it's frontmatter:
---
title: My Latest Article
date: today
_path: "/my-latest-article"
---
Luckily, we can write a simple Nitro plugin that will do this transform automatically.
Nitro is the server that Nuxt uses internally. We can hook into it's processing pipeline
and do a bit of tweaking.
However, doing this breaks queryContent calls if we're filtering based on the path,
since queryContent is looking at the _path property we've just modified. This is why
we want to keep that original directory around.
We can modify our queryContent calls to filter on this new _original_dir property:
// Before
queryContent('/articles')
// After
queryContent()
.where({
_original_dir: { $eq: '/articles' },
});
Pro tip: use nuxi clean to force Nuxt Content to re-fetch and re-transform all of your
content.
Nuxt Content 2 gives us an effortless way to query our content using the
queryContent method:
// composables/useArticles.js
Here, I’ve created a composable called useArticles for my blog, which grabs all of
the content inside of the content/articles/ directory.
First, we’re using a where clause to filter out all the articles we don’t want. Sometimes
I will add an article before I want it to be “published” to the site.
I do this by setting the date in the future and then only taking articles before “today”
using this clause:
Second, some articles are the newsletters I write each week. Others are pieces of
content that I want to keep in the articles folder but don’t want to be published.
---
newsletter: true # This is a newsletter
---
---
ghost: true # This content won't appear on the site
---
Third, we use the only clause to grab just the fields we need. By default, the
queryContent method returns a lot of data, including the entire piece of content itself,
so this can make a big difference in payload size.
Lastly, as you have probably guessed, we have a sort clause to sort the articles so
the most recent ones appear last.
The queryContent composable has more options than this, which you can read about
on the docs.
There are two kinds of utilities in Nuxt: Vue utilities and server utilities (or Nitro
utilities).
Vue utilities live in the ~/utils folder and are auto-imported into the Vue context of
your application:
// ~/utils/toArray.js
You can use them in any Vue components, route middleware (since it runs in Vue
Router), or composables:
You can also define your utils using named exports, putting multiple in a single file:
// ~/utils/utils.js
However, these two folders, ~/utils and ~/composables , aren’t special in any way.
They’ve just been configured to be auto-imported. You can configure any directory
you want to be auto-imported in the same way.
Server utilities work in essentially the same way, except they’re only available in the
Nitro context: server routes and server middleware. You define them in the ~/server/
utils directory:
// ~/server/utils/toArray.js
Let's say you want to create a Twitte/X bot that will automatically tweet out your
favourite quotes.
Instead of giving the bot all of your login information (which is not a good idea), you
can use OAuth to securely provide access to your Twitter/X account.
Now that we have all of our pieces in place, we need to see how they interact with
each other.
Here’s what the OAuth flow would look like for our Twitter bot example:
1. The user clicks on a "Log in with Twitter" button in the Twitter bot app or
service.
2. The app or service redirects the user to Twitter's authorization server.
3. The user logs in to their Twitter account (or confirms that they are already
logged in).
In this case, once we get that token back, we’re set. That access token contains all of
the information we need about the user, and proves their identity.
This is how I use Github in my course platform. I only need to verify each user’s
identity. I don’t actually need to access any of their Github data at all.
It doesn’t help that they both shorten to “auth”, so it’s not always clear which one
we’re referring to!
Most applications need both. You only want paying customers to access your app, so
you need authorization. But you need authentication to determine who is your
customer and who isn’t.
Authentication can be hard, which is why most apps use OAuth so that we can rely
on third parties like Google, Twitter/X or Github to provide the identity for us. We
could also implement this ourselves, but that involves managing passwords which is a
whole can of worms.
Authorization can be a simple check in the database. Does this email address /
Github user have access to this feature?
Just make sure to do this on the server — the client-side is never all that secure
because the end user has complete access to everything once it’s on their device.
If you want your plugins to leverage the hydration lifecycle, you can use the
useHydration composable:
useHydration(
'unique-key',
It’s actually a pretty straightforward composable, giving you insight into how hooks
and the Nuxt payload work (I’ve removed types for clarity):
if (import.meta.server) {
nuxtApp.hooks.hook('app:rendered', () => {
nuxtApp.payload[key] = get()
})
}
if (import.meta.client) {
nuxtApp.hooks.hook('app:created', () => {
set(nuxtApp.payload[key])
})
}
}
If we’re on the server, use the “app:rendered” hook to wait until SSR is done. Then, we
On the client we do the reverse. We wait for the “app:created” hook, then take our
payload and pass it to the set function.
From the user’s perspective these functions might make more sense switched around,
but seeing the actual code here makes it easier to see what’s going on.
However, the one caveat is that this only works in the context of a Nuxt plugin, not a
component. This is because the Vue app is actually mounted after the app:created
hook has run.
To do this in a component, we can instead wait for the app:mounted hook which is only
run on the client. And since the app:rendered hook is only called on the server, we can
get rid of all the extra conditionals too:
nuxtApp.hooks.hook('app:rendered', () => {
nuxtApp.payload[key] = get()
});
nuxtApp.hooks.hook('app:mounted', () => {
set(nuxtApp.payload[key])
})
}
If you need some code to run on the client before Nuxt does any initialization, you can
use the onPreHydrate composable:
onPreHydrate(() => {
// Manipulate the DOM in a specific way
const root = document.querySelector('#__nuxt');
root.addEventListener('click', () => alert('Hello World!'));
});
It works by stringifying the entire function and inlining it into the initial HTML sent to
the client. This means that you cannot use any external dependencies (no closures)
and cannot rely on Vue or Nuxt to exist, since it runs before they are initialized:
onPreHydrate(() => {
// This will not work, since Vue hasn't been loaded yet!
// Vanilla JS only
const count = ref(0);
const doubleCount = computed(() => count.value * 2);
});
Github →
Stackblitz →
2. Layers
Layers are one of the best ways to organize large
Nuxt projects.
Github →
Stackblitz →
3. Keyed Composables
See how you can use Nuxt's key auto-injection in
your own composables.
Github →
Stackblitz →
4. Routing Precedence
Nuxt's file-based routing has some interesting
edge cases around naming collisions and child
routes.
Github →
Stackblitz →
5. Keeping Pages Alive
Because Nuxt has client-side routing, we can use
the keepalive component on pages. This makes it
possible to do some interesting things!
Github →
Stackblitz →
Github →
Stackblitz →
7. Prefetching Components
This demo shows how we can prefetch lazy
components if we think they'll be needed.
Github →
Stackblitz →