Introduction
When your Nuxt app starts, you often need to configure global services, inject utilities into the Vue instance, or set up integrations before any component renders. That's where Nuxt plugins come in. Understanding Nuxt plugins: what every Vue & Nuxt developer should know will help you write cleaner, more maintainable initialization code that scales across your application.
Plugins are functions that Nuxt calls automatically during app startup, before any pages load. They're the right place for setting up HTTP interceptors, registering global components, injecting helper functions, or initializing third-party libraries. Without them, you'd scatter initialization logic across layouts, middleware, or components—a pattern that quickly becomes messy.
The plugins/ folder auto-registers
One of Nuxt's developer-friendly features is automatic plugin registration. Any file you create in the plugins/ folder is automatically discovered and executed at startup. You don't need to manually import or register them in nuxt.config.ts unless you have specific ordering or configuration needs.
Let's say you're building an e-commerce platform and you need to initialize a logging service before the app runs. Create a file in plugins/:
// plugins/logger.ts
export default defineNuxtPlugin(() => {
const log = (message: string, level: 'info' | 'warn' | 'error' = 'info') => {
const timestamp = new Date().toISOString()
console.log(`[${timestamp}] [${level.toUpperCase()}] ${message}`)
}
return {
provide: {
logger: { log }
}
}
})
That's it. Nuxt scans the plugins/ folder, finds logger.ts, and runs it during initialization. No configuration needed. Inside your components or server routes, you can access the logger via $logger:
// components/ProductCard.vue
export default {
setup() {
const { $logger } = useNuxtApp()
$logger.log('Product card mounted', 'info')
}
}
The auto-registration pattern keeps your setup code colocated and reduces boilerplate. However, if you need fine-grained control—for example, ensuring one plugin runs before another—you can use numeric prefixes.
Provide helpers to the app
Plugins inject utilities and services into the Nuxt app using the provide key. This makes them globally available without manual imports. The pattern follows Vue 3's composition API provide/inject system, but at the application level.
Consider a SaaS platform where you need an API client available throughout your app. A plugin is the ideal place to instantiate and provide it:
// plugins/api-client.ts
import axios from 'axios'
export default defineNuxtPlugin(() => {
const apiClient = axios.create({
baseURL: process.env.NUXT_PUBLIC_API_URL,
timeout: 10000,
})
// Add request interceptor for auth tokens
apiClient.interceptors.request.use((config) => {
const token = useAuthStore().token
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
// Add response interceptor for error handling
apiClient.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
// Handle unauthorized
navigateTo('/login')
}
return Promise.reject(error)
}
)
return {
provide: {
api: apiClient
}
}
})
Now, anywhere in your app—a composable, a component, a server route—you can use $api:
// composables/useUser.ts
export const useUser = () => {
const { $api } = useNuxtApp()
const user = ref(null)
const fetchUser = async (id: string) => {
const response = await $api.get(`/users/${id}`)
user.value = response.data
}
return { user, fetchUser }
}
This approach centralizes HTTP configuration, keeps interceptors consistent, and makes testing easier. You're providing a single, configured instance rather than scattering axios setup across multiple files.
Client-only or server-only suffixes
Nuxt runs code on both the server and the client. Sometimes a plugin should only run in one environment. Nuxt provides conventions for this: suffix your filename with .client.ts or .server.ts.
Imagine you're working on a ride-hailing app. You need a geolocation service, but the browser's navigator.geolocation API doesn't exist on the server. Create a client-only plugin:
// plugins/geolocation.client.ts
export default defineNuxtPlugin(() => {
const getLocation = () => {
return new Promise((resolve, reject) => {
if (!navigator.geolocation) {
reject(new Error('Geolocation not supported'))
return
}
navigator.geolocation.getCurrentPosition(
(position) => {
resolve({
latitude: position.coords.latitude,
longitude: position.coords.longitude
})
},
(error) => reject(error)
)
})
}
return {
provide: {
geolocation: { getLocation }
}
}
})
This plugin only loads and runs on the browser. If your code tries to call $geolocation during server-side rendering, it won't crash; the hook simply won't execute server-side.
Conversely, a .server.ts suffix ensures a plugin runs only on the server. For example, you might initialize a database connection pool or warm up caches:
// plugins/database.server.ts
import { createPool } from 'pg'
export default defineNuxtPlugin(() => {
const pool = createPool({
host: process.env.DB_HOST,
port: process.env.DB_PORT,
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
})
return {
provide: {
db: pool
}
}
})
These suffixes prevent bundling unnecessary code and avoid runtime errors from accessing browser or server APIs in the wrong context.
Order with numeric prefixes
When you have multiple plugins and one depends on another, you need control over execution order. Nuxt allows numeric prefixes to enforce sequencing. Plugins with lowercase prefixes run first, followed by those without prefixes, then uppercase prefixes.
In a travel booking platform, imagine you need to initialize authentication before any other service uses it:
// plugins/01-auth.ts
export default defineNuxtPlugin(() => {
const authStore = useAuthStore()
const initAuth = async () => {
const token = localStorage.getItem('authToken')
if (token) {
await authStore.validateToken(token)
}
}
return {
provide: {
auth: { initAuth }
}
}
})
// plugins/02-api-client.ts
export default defineNuxtPlugin(() => {
const { $auth } = useNuxtApp()
const apiClient = axios.create({
baseURL: process.env.NUXT_PUBLIC_API_URL
})
// The auth plugin is already initialized, so we can safely use it
apiClient.interceptors.request.use((config) => {
const authStore = useAuthStore()
if (authStore.isAuthenticated) {
config.headers.Authorization = `Bearer ${authStore.token}`
}
return config
})
return {
provide: {
api: apiClient
}
}
})
The 01-auth.ts plugin runs first, setting up authentication. The 02-api-client.ts plugin runs second and can rely on auth being initialized. This prevents race conditions and makes dependencies explicit.
If your plugin uses async operations, you can use defineNuxtPlugin with an async function, and Nuxt will wait for it to complete before hydrating the app:
// plugins/01-auth.ts
export default defineNuxtPlugin(async () => {
const authStore = useAuthStore()
const token = localStorage.getItem('authToken')
if (token) {
await authStore.validateToken(token)
}
return {
provide: {
auth: authStore
}
}
})
Common mistakes
Not using provide correctly: New developers sometimes return an object directly instead of wrapping it in provide. Always return { provide: { ... } } so your helpers are accessible via useNuxtApp().
Forgetting about SSR context: If you write a plugin without considering server-side rendering, it may work locally but fail in production. Test SSR behavior early, and use .client.ts or .server.ts suffixes when appropriate.
Making plugins too fat: Plugins should initialize services or set global utilities. Don't put business logic or heavy computations in plugins. Keep them lean and focused. If your plugin file exceeds 100–150 lines, consider refactoring into a composable or utility module.
Ignoring plugin ordering: When you have three plugins and don't use numeric prefixes, their execution order is undefined. This can cause subtle bugs where one plugin expects another to have already run. Always use prefixes when there's a dependency.
Returning utilities directly instead of inside provide: If you return { logger: loggerInstance } instead of { provide: { logger: loggerInstance } }, your plugin won't inject the helper globally and only the plugin's internal code can use it.
Key takeaways
- Plugins in the
plugins/folder are auto-registered and run at startup before any pages render. - Use the
providekey to inject global utilities, services, or helpers available throughout your app viauseNuxtApp(). - Apply
.client.tsand.server.tssuffixes to run code only in the browser or only on the server, preventing SSR errors and reducing bundle size. - Use numeric prefixes like
01-,02-to enforce plugin execution order when one plugin depends on another. - Keep plugins focused on initialization: HTTP clients, authentication, logging, third-party SDKs, and global helpers belong here, not business logic.
- Test your plugins in both SSR and client contexts to avoid surprises in production.






Comments
Loading comments…