Skip to main content

What are plugins in Nuxt?

Nuxt plugins are files in the plugins/ directory that run once during app initialization, registering Vue plugins, utilities, or global helpers before any component mounts.

Theory

TL;DR

  • Think of plugins as airport ground crew: they set up services (auth, notifications, API clients) before the app starts, so every page has access without asking
  • Plugins run once at startup; composables run per component; middleware runs per route
  • Use a plugin when you need a Vue integration (vueApp.use()) or a global injectable (provide)
  • Suffix .client.ts for browser-only code, .server.ts for SSR-only code
  • Nuxt sorts plugins alphabetically; numeric prefixes (01.auth.ts) control order

Quick example

javascript
// plugins/toast.client.ts - runs only in the browser import Toast from 'vue-toastification' export default defineNuxtPlugin((nuxtApp) => { nuxtApp.vueApp.use(Toast, { timeout: 3000 }) // Toast is now registered globally on the Vue app }) // In any component or page: const { $toast } = useNuxtApp() $toast.success('User saved!')

One registration, available everywhere. No imports in individual components.

Key difference

Plugins execute during Nuxt's startup phase, before hydration. The Vue app gets the plugin before any component tree is built. Composables, by contrast, run on demand inside component setup functions. Put per-component logic in a plugin and you bloat every page load. Call a composable at startup time like a plugin and you hit context errors.

When to use

  • Vue plugin integration (vue-toastification, pinia, i18n) -> plugin with vueApp.use()
  • Global API client available everywhere -> plugin with provide: { api }
  • Reusable stateless logic -> composable
  • Route-specific logic -> middleware
  • Browser-only code (analytics, localStorage) -> plugin.client.ts
  • Server-only code (database connections) -> plugin.server.ts

How Nuxt loads plugins

Nuxt scans plugins/ at build time and registers each file automatically. At startup, it executes them sequentially in alphabetical order. Client plugins run after hydration (post-mount), server plugins run per request inside the Nitro engine. The provide object from each plugin gets merged into useNuxtApp() context via Vue's app.provide(). That's why $toast or $api appears on useNuxtApp() without any explicit import in your component.

Nuxt 3.10+ also supports parallel: true inside defineNuxtPlugin for plugins with no dependencies on each other. On large apps this cuts startup time noticeably.

Common mistakes

1. Missing .client.ts suffix for browser-only code

javascript
// Wrong: plugins/analytics.ts // Runs on SSR AND client = double tracking, crashes on window references // Fix: plugins/analytics.client.ts export default defineNuxtPlugin(() => { window.analytics.init() // window is safe here })

2. Wrong plugin order for dependencies

javascript
// Wrong: plugins/api.ts loads before plugins/auth.ts // api.ts tries to read an auth token that doesn't exist yet // Fix: rename to 01.auth.ts and 02.api.ts // Nuxt sorts alphabetically, numeric prefixes win

This is the most common silent bug I see in Nuxt repos: auth and API loaded in the wrong order because no one added numeric prefixes until something broke in production.

3. Forgetting to return provide

javascript
// Wrong: export default defineNuxtPlugin(() => { const api = { getUser: (id) => $fetch(`/api/users/${id}`) } // api is defined but never exposed to the app }) // Fix: export default defineNuxtPlugin(() => { const api = { getUser: (id) => $fetch(`/api/users/${id}`) } return { provide: { api } } }) // Now: const { $api } = useNuxtApp() works in any component

4. Data fetching directly inside plugin initialization

Avoid fetch calls inside plugin setup code. They run outside the component lifecycle, so useAsyncData and useFetch are not available. Move data fetching to composables where they have access to the proper request context.

Real-world usage

  • vue-toastification -> global toast notifications in e-commerce checkouts
  • Pinia -> store setup in SaaS dashboards (common in nuxtjs/starter repos)
  • Supabase -> auth client available app-wide in CMS and content sites
  • Day.js -> date formatting utility injected globally in blogging platforms
  • Sentry -> error tracking initialized once for the whole app

Follow-up questions

Q: How does Nuxt determine plugin execution order?
A: Alphabetically by filename. Prefix with numbers to control it: 01.auth.ts runs before 02.api.ts.

Q: What is the difference between provide and vueApp.use()?
A: vueApp.use() registers a Vue plugin (directives, components, global install logic). provide injects a value into Nuxt's context, accessible via useNuxtApp(). You often use both in the same plugin.

Q: Can a Nuxt plugin be async?
A: Yes. Pass async (nuxtApp) => { ... } to defineNuxtPlugin. Nuxt awaits async plugins before rendering, so keep them fast or every request will wait on them.

Q: How do plugins handle SSR hydration mismatches?
A: Use .client.ts for plugins that touch browser APIs. For shared state between server and client, use useState() inside the plugin rather than a plain variable.

Q: What is parallel: true and when do you use it?
A: Added in Nuxt 3.10. Lets a plugin run in parallel with others instead of waiting for the previous one to finish. Use it for plugins with no dependencies on each other. Reduces startup time on apps with many plugins.

Examples

Registering a Vue plugin

javascript
// plugins/toast.client.ts import Toast from 'vue-toastification' export default defineNuxtPlugin((nuxtApp) => { nuxtApp.vueApp.use(Toast, { timeout: 3000 }) }) // pages/index.vue const { $toast } = useNuxtApp() async function saveUser(data) { await $fetch('/api/user', { method: 'POST', body: data }) $toast.success('Saved!') }

One import, one registration, available in every page and component. This covers the majority of plugin use cases.

Global API client with provide

javascript
// plugins/supabase.ts import { createClient } from '@supabase/supabase-js' export default defineNuxtPlugin(() => { const config = useRuntimeConfig() const client = createClient( config.public.supabaseUrl, config.public.supabaseKey ) return { provide: { supabase: client } } }) // components/Login.vue const { $supabase } = useNuxtApp() async function login(email, password) { const { data, error } = await $supabase.auth.signInWithPassword({ email, password }) if (error) console.error(error.message) }

The Supabase client initializes once, reuses its connection pool, and is available everywhere without re-importing. Standard pattern in NuxtContent-based CMS starters.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?