Nuxt has so many amazing features. This article is a compilation of 24 tips to help you save time and write better Nuxt apps.
Get notified when we release new tutorials, lessons, and other expert Nuxt content.
We know that Nuxt is a fantastic tool.
But it has so many amazing features that it’s hard to keep track of them all.
That’s why I’ve compiled this giant list of 24 Nuxt tips for you — use them to save time and write better Nuxt apps.
We cover a lot of topics here, including:
/assets
vs. /public
directoryruntimeConfig
vs. app.config
NuxtLink
components that no one is talking aboutOf course, there is so much more!
Which tip is your favourite?
Not all your components need to be loaded immediately.
With Nuxt we can defer loading by adding Lazy as a prefix.
Nuxt does all the heavy-lifting for us!
<!-- Loads as soon as possible -->
<Modal v-if="showModal" />
<!-- Only loads when showModal = true -->
<LazyModal v-if="showModal" />
By configuring Nitro directly, we can have only some routes pre-rendered.
Every other route will use hybrid rendering, like normal:
export default defineNuxtConfig({
nitro: {
prerender: {
routes: ['/about', '/blog'],
},
},
});
In Nuxt we can use a simple, but powerful, built-in key-value storage:
const storage = useStorage();
const key = `session:token`;
// Save the value
await storage.setItem(key, sessionToken);
// ...and retrieve it in a different API call
const token = await storage.getItem(key);
By taking advantage of auto-imports, we can quickly access the route and user information without needing to manually import them.
This helps make our code more organized, efficient, and readable.
<script setup>
// We can use the imported route and user without having to import it ourselves
const { path, params } = useRoute();
const userData = useCustomComposable();
</script>
Nuxt 3 allows developers to reactively control the <head>
of their application with the useHead
composable.
This lets you use a dynamic titleTemplate
or dynamic script tags:
useHead({
titleTemplate: (title) => `${title} | My Website`,
script: [
{
src: 'https://www.scripts.com/some/other/script.js',
body: true
}
]
})
The useRoute composable in Nuxt.js 3 makes it easy to grab info from the route, and also query parameters.
Here's an example:
const route = useRoute();
console.log(route.fullPath);
// https://www.website.com/?search=hello%20there
console.log(route.query.search);
// there
Using NuxtErrorBoundary
components around distinct chunks of functionality in your app allows you to handle a group of errors together, providing better UX.
This lets you contain errors in your app and handle them in specific ways instead of using a generic error page.
<NuxtErrorBoundary>
<NuxtPage />
<template #error="{ error }">
<div>
<p>
Oh no, something broke when loading the lesson!
<code>{{ error }}</code>
</p>
<p>
<button
class="hover:cursor-pointer"
@click="clearError(error)"
>
Go to the first lesson
</button>
</p>
</div>
</template>
</NuxtErrorBoundary>
Nuxt uses the NuxtPage
component to render a page from your app’s pages/
directory.
Adding nested NuxtPage
components lets you match to multiple pages in your folder hierarchy, allowing for more flexible routing:
// pages/one/two/three.vue
<template>
<!-- The nested NuxtPage renders child routes -->
<NuxtPage />
</template>
Nested routes in Nuxt can be thought of as a folder hierarchy, which can help organize and structure your application.
For example, the route /one/two/three
is equivalent to this folder structure:
one/
- two/
- - three/
Nuxt 3 offers two options for managing assets in your web app:
~/assets
folder~/public
folderChoose assets
folder if the assets need processing, change often, and don’t require a specific filename.
Otherwise, use the public
directory.
// Using ~/assets
<script setup>
import image from '~/assets/image.png';
</script>
<template>
<img :src="image" />
</template>
// Using ~/public
<template>
<img src="/image.png" />
</template>
The ~/assets
folder in Nuxt 3 is ideal for assets that need processing.
eg. images, stylesheets, icons, and fonts.
When you import an asset from this folder, the bundler processes the file, generates a new filename with a hash, and replaces the import with the new filename.
import image from '~/assets/image.png';
import style from '~/assets/style.css';
The ~/assets
folder has the added benefit of catching missing assets during the build process.
If an asset is missing, you'll get a build error, which helps you maintain your app's integrity.
This won't happen with assets in the ~/public
folder since they aren't processed.
// Importing from ~/assets will catch missing assets
import missingImage from '~/assets/missing-image.png'; // Build error
// Referencing from ~/public won't catch missing assets
// In your template: <img src="/missing-image.png" /> // No build error
The ~/public
folder is great for assets that don't need processing and should be served directly from the root of your app.
Files in this folder are not modified and are copied directly to the build output.
// Accessing files from ~/public
yourwebsite.com/robots.txt
yourwebsite.com/favicon.ico
yourwebsite.com/sitemap.xml
You can also encapsulate a lot of these different configurations into your own link components if you want, using defineNuxtLink
:
// ~/components/MyLink.ts
// Only colour prefetched links during development
export default defineNuxtLink({
componentName: 'MyLink',
prefetchedClass: process.env.NODE_ENV === 'development'
? 'prefetched'
: undefined,
});
Here we create our own MyLink
component that will set a special class on prefetched links, but only during development.
You can do a lot more with defineNuxtLink
:
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.
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:
<NuxtLink to="/articles" prefetch>Articles</NuxtLink>
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:
<NuxtLink to="/articles" :prefetch="false">Articles</NuxtLink>
We can also do the same thing with noPrefetch
:
<NuxtLink to="/articles" no-prefetch>Articles</NuxtLink>
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!
Did you know that NuxtLink
can also handle external links?
It will automatically add noopener
and noreferrer
for security.
It auto-detects external links, but there’s also an external
prop if needed.
<NuxtLink to="www.masteringnuxt.com" external>
Mastering Nuxt 3!
</NuxtLink>
For external links, it will automatically adding in noopener
and noreferrer
attributes for security:
<!-- Using an anchor tag -->
<a href="www.masteringnuxt.com" rel="noopener noreferrer">Mastering Nuxt 3</a>
<!-- Replace with NuxtLink -->
<NuxtLink to="www.masteringnuxt.com">Mastering Nuxt 3</NuxtLink>
key
parameterThe key
parameter is an optional argument you can provide to the useAsyncData
and useFetch
composables.
Internally, the key
parameter is used to create a unique identifier for the fetched data. This helps optimize the performance of your application by reducing unnecessary data fetching on the client if the data has already been fetched on the server.
Imagine you're building a music production app where users can select a project and view the associated tracks. You can use the key
parameter to ensure that the tracks are updated whenever the selected project changes:
<template>
<div>
<select v-model="selectedProject">
<option v-for="project in projects" :value="project.id" :key="project.id">
{{ project.name }}
</option>
</select>
<div v-if="pending">Loading tracks...</div>
<div v-else-if="error">{{ error.message }}</div>
<div v-else>
<ul>
<li v-for="track in tracks" :key="track.id">{{ track.name }}</li>
</ul>
</div>
</div>
</template>
const selectedProject = ref(1)
const projects = [
{ id: 1, name: 'Project A' },
{ id: 2, name: 'Project B' },
{ id: 3, name: 'Project C' },
]
const { data: tracks, pending, error } = useAsyncData(
'tracks',
() => fetch(`https://api.example.com/projects/${selectedProject.value}/tracks`)
)
Here, the key
parameter is tracks
.
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.
To log in a user with Github as the OAuth provider, we can use the Supabase auth client:
const supabase = useSupabaseAuthClient();
const login = async () => {
const { error } = await supabase.auth.signInWithOAuth({
provider: 'github',
});
if (error) {
console.error(error);
// Handle error, e.g., show a notification to the user
}
};
There’s really nothing more to it!
The useAsyncData
composable is a powerful composable provided by Nuxt that allows you to fetch data asynchronously in your components.
This is particularly useful when you need to fetch data from an API or perform other asynchronous tasks before rendering your component.
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="!pending && !error">
<li v-for="instrument in instruments" :key="instrument.id">
{{ instrument.name }}
</li>
</ul>
<p v-if="pending">Loading...</p>
<p v-if="error">Error: {{ error.message }}</p>
</div>
</template>
const { data: instruments, pending, error } = useAsyncData(
'instruments',
() => fetch('https://api.example.com/instruments')
)
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 pending
and error
properties, which can be used to display loading and error states in our template.
You can check out the documentation for more information.
This is another composable provided by Nuxt that simplifies data fetching in your components.
It's a wrapper around useAsyncData
and provides some additional features, such as automatic key generation based on the URL and fetch options.
Here's an example of how you might use useFetch
in a music production app to fetch a list of tracks for a specific project:
<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>
const projectId = 1
const { data: tracks, pending, error } = useFetch(
`https://api.example.com/projects/${projectId}/tracks`
)
In the example above, we're checking if pending
is true and displaying a loading message if so. Additionally, we're checking if there's an error and displaying the error message if one occurs.
To ensure your component updates when the project ID changes, you can pass in the projectId
as a ref
instead:
const projectId = ref(1)
const { data: tracks, pending, error } = useFetch(
() => `https://api.example.com/projects/${projectId.value}/tracks`
)
This way, if the projectId
value changes, the URL will update accordingly and the data will be fetched again.
If you want, you can check out the documentation for more information.
There are a few different Vite plugins for compressing images, but for this tip we’ll use [vsharp](https://github.com/jw-12138/vite-plugin-vsharp)
.
To install, run pnpm add -D vite-plugin-vsharp
.
Then, we update our configuration to look something like this:
import vsharp from 'vite-plugin-vsharp';
export default defineNuxtConfig({
// 👇 Add vsharp to the vite plugins
vite: {
plugins: [vsharp()],
},
// The rest of your Nuxt config
runtimeConfig: {
// ...
},
modules: [
// ...
],
});
That’s it!
As long as we make sure our images are in /assets
and then imported into our components, they’ll be compressed during the build.
That’s a big win for a small config change!
Here’s what our useFetchWithCache
composable looks like:
// ~/composables/useFetchWithCache.ts
import { StorageSerializers } from '@vueuse/core';
export default async <T>(url: string) => {
// Use sessionStorage to cache data
const cached = useSessionStorage<T>(url, null, {
serializer: StorageSerializers.object,
});
if (!cached.value) {
const { data, error } = await useFetch<T>(url);
if (error.value) {
throw createError({
...error.value,
statusMessage: `Could not fetch data from ${url}`,
});
}
// Update the cache
cached.value = data.value as T;
} else {
console.log(`Getting value from cache for ${url}`);
}
return cached;
};
Here are two examples for how this might be used:
// Get all users
const user = useFetchWithCache<User[]>('/api/users')
// Or fetch the settings
const settings = useFetchWithCache<Settings>('/api/settings')
Read the full tutorial to see how this all works in detail.
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.
However, Nuxt doesn't stop at the initial SSR.
It also loads the entire app as a 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 and SSR worlds.
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>
To better understand the differences and similarities between runtimeConfig
and app.config
, let's take a look at this feature comparison table (taken from the Nuxt documentation):
Feature | runtimeConfig | app.config |
---|---|---|
Client Side | Hydrated | Bundled |
Environment Variables | ✅ Yes | ❌ No |
Reactive | ✅ Yes | ✅ Yes |
Types support | ✅ Partial | ✅ Yes |
Configuration per Request | ❌ No | ✅ Yes |
Hot Module Replacement | ❌ No | ✅ Yes |
Non primitive JS types | ❌ No | ✅ Yes |
Both runtimeConfig
and app.config
allow you to expose variables to your application. However, there are some key differences:
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.runtimeConfig
values are hydrated on the client side during run-time, while app.config
values are bundled during the build process.app.config
supports Hot Module Replacement (HMR), which means you can update the configuration without a full page reload during development.app.config
values can be fully typed with TypeScript, whereas runtimeConfig
cannot.To decide whether to use runtimeConfig or app.config, I’d think about it this way:
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:
export default defineNuxtConfig({
runtimeConfig: {
// The private keys which are only available server-side
shoeStoreApiSecret: 'my-secret-key',
// Keys within public are also exposed client-side
public: {
shoeStoreApiBase: '/shoe-api'
}
}
})
To access runtimeConfig
values within your application, you can use the useRuntimeConfig
composable:
<script setup lang="ts">
const { shoeStoreApiBase } = useRuntimeConfig();
console.log(shoeStoreApiBase); // /shoe-api
</script>
Note that you can’t access a private key on the client-side:
<script setup lang="ts">
const { shoeStoreApiSecret } = useRuntimeConfig();
console.log(shoeStoreApiSecret); // undefined
</script>
But you can access all values in a server route:
export default defineEventHandler(async (event) => {
const { shoreStoreApiSecret } = useRuntimeConfig();
console.log(shoeStoreApiSecret); // my-secret-key
});
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, and don’t forget to add in PUBLIC
if it’s a value in the public
field of your config:
NUXT_PUBLIC_SHOE_STORE_API_BASE_URL = "https://api.shoestore.com"
NUXT_SHOE_STORE_API_SECRET = "my-secret-key"
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
export default defineAppConfig({
theme: {
primaryColor: '#ababab'
}
})
To access app.config
values within your application, you can use the useAppConfig
composable:
<script setup lang="ts">
const appConfig = useAppConfig()
</script>
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'
}
}
}
If you’re writing a module that needs config, you can also provide a type for your module.
If you enjoyed these tips, please do me a huge favour and share the article!
Nuxt is an incredible tool, and everyone should know these tips so they can use it to the fullest.
If you want to learn more about Nuxt, the best way to do that is through the Mastering Nuxt 3 course.