Different lifecycles, different responsibilities
Nuxt 3 is powerful, but the ecosystem can feel a bit confusing at first—especially three concepts that are often mixed up: Middleware, Plugin, and Composable.
They hook into different parts of the app lifecycle and carry different responsibilities.
In this post, we’ll explain what each does, how they differ, and when to use which—backed by examples.
1) Middleware — Manage page transitions
Middleware integrates with Nuxt’s routing system and runs before a page loads. It’s typically used for authorization, redirect control, or pre-validation.How it works
Before navigating to a page, middleware runs. If it allows, the page loads; otherwise, you redirect.// middleware/auth.global.ts
export default defineNuxtRouteMiddleware((to, from) => {
const user = useSupabaseUser()
if (!user.value) return navigateTo('/login')
})
Types
- Route Middleware: Runs per-page or globally (
middleware/). - Server Middleware: Runs at the API/server layer (
server/middleware/).
When to use
- Auth / redirect checks.
- Route-based permission systems.
- Validating data before the page renders.
/login.”2) Plugin — Extend the app
A Plugin adds global features, libraries, or services to your app. It can touch the Vue instance, Nuxt context, or global app level.How it works
Plugins load at app startup and give you access to the app instance.// plugins/axios.ts
import axios from 'axios'
export default defineNuxtPlugin((nuxtApp) => {
const api = axios.create({ baseURL: '/api' })
nuxtApp.provide('api', api)
})
Now anywhere in the app:const { $api } = useNuxtApp()
you can use this service.When to use
- Integrating third-party libs (Axios, dayjs, GSAP, etc.).
- Adding global mixins or helpers.
- Defining app-wide services.
$api or $auth to use everywhere.3) Composable — Reuse logic
A Composable is a Composition-API helper. It packages repeatable logic into a function you can share across components—think logic module, not component.How it works
Files live undercomposables/. This folder is auto-imported in Nuxt 3.// composables/useCounter.ts
export const useCounter = () => {
const count = ref(0)
const inc = () => count.value++
const dec = () => count.value--
return { count, inc, dec }
}
Then in any component:const { count, inc } = useCounter()
When to use
- Sharing the same logic across multiple components.
- Centralizing LocalStorage, API calls, form validation, etc.
- Building a more modular, testable architecture.
useAuth(), useTheme(), useForm() are classic composables.Comparison table
| Property | Middleware | Plugin | Composable |
|---|---|---|---|
| When it runs | On route change | On app load | When a component calls it |
| Purpose | Route control | Define global services | Share logic |
| SSR support | ✅ | ✅ | ✅ |
| Scope | Route / Server | App-wide | Local or global |
| Location | middleware/ | plugins/ | composables/ |
Example scenario
You’re implementing user auth:- Access control → Middleware (
auth.global.ts) - Auth service setup → Plugin (
plugins/auth.ts) - Reusable login/logout logic → Composable (
useAuth.ts)
middleware/auth.global.ts → Access control
plugins/auth.ts → $auth service definition
composables/useAuth.ts → Login / logout logic
They work together—without stepping on each other.Commonly confused cases
- Using a plugin inside a composable: Yes—e.g., consume
$apiinside a composable. - Calling a composable from middleware: Also yes—middleware runs during setup.
- Reactive state in a plugin: Possible, but be careful—plugins are global, so shared state can leak.
useState(). It isolates per-request state during SSR.Conclusion
In Nuxt 3:- Middleware → “Check before you go.”
- Plugin → “Extend the app.” 🔌
- Composable → “Share logic.”