Skip to main content
Engineering7 min readMarch 3, 2026

Nuxt Middleware and Plugins: The Difference and When to Use Each

A clear breakdown of Nuxt route middleware vs server middleware vs plugins — what each does, when to use which, and patterns for authentication, logging, and initialization.

James Ross Jr.

James Ross Jr.

Strategic Systems Architect & Enterprise Software Developer

Middleware and plugins in Nuxt serve different purposes, but the names obscure that difference for developers new to the framework. I have seen middleware used for things that belong in plugins, plugins used for things that belong in composables, and server middleware confused with route middleware. Getting this right leads to cleaner code and fewer subtle bugs.

Let me draw the lines clearly.

Three Types of Middleware

Nuxt has three distinct middleware systems:

Route middleware runs on client-side navigation between pages. It lives in the middleware/ directory and uses defineNuxtRouteMiddleware.

Server middleware runs on every incoming HTTP request to the Nitro server. It lives in server/middleware/ and uses defineEventHandler.

Plugin middleware does not technically exist as a term, but people often reach for plugins when they want route middleware. I will clarify that distinction below.

Route Middleware

Route middleware intercepts navigation between pages. Its purpose is to control whether a navigation happens — redirect, abort, or allow it.

// middleware/auth.ts
export default defineNuxtRouteMiddleware(async (to, from) => {
  const { data: session } = await useAuth()

  // User is not authenticated
  if (!session.value) {
    // Redirect to login with the intended destination
    return navigateTo(`/login?redirect=${encodeURIComponent(to.fullPath)}`)
  }

  // User lacks required role
  if (to.meta.requiredRole && session.value.user.role !== to.meta.requiredRole) {
    throw createError({ statusCode: 403, statusMessage: 'Forbidden' })
  }
})

Apply middleware to specific pages with definePageMeta:

<script setup lang="ts">
definePageMeta({
  middleware: ['auth'],
  requiredRole: 'admin',
})
</script>

Or make it global (runs on every navigation) by naming it with a .global suffix:

// middleware/analytics.global.ts
export default defineNuxtRouteMiddleware((to) => {
  // Track every page view
  useTrackPageView(to.path)
})

Key points about route middleware:

  • It runs in the browser during client-side navigation
  • It runs on the server during SSR for the initial page request
  • It should not do heavy work — it blocks navigation until it completes
  • It cannot be used for server-only operations (database access, etc.) when running client-side

Server Middleware

Server middleware runs on every request before your API routes handle it. It lives in server/middleware/ and has access to the full H3 event object.

// server/middleware/logger.ts
export default defineEventHandler((event) => {
  const start = Date.now()
  const url = getRequestURL(event)
  const method = getMethod(event)

  event.node.res.on('finish', () => {
    const duration = Date.now() - start
    const status = event.node.res.statusCode
    console.log(`[${new Date().toISOString()}] ${method} ${url.pathname} ${status} ${duration}ms`)
  })
})
// server/middleware/cors.ts
export default defineEventHandler((event) => {
  const allowedOrigins = ['https://yourdomain.com', 'https://app.yourdomain.com']
  const origin = getHeader(event, 'origin')

  if (origin && allowedOrigins.includes(origin)) {
    setResponseHeader(event, 'Access-Control-Allow-Origin', origin)
  }

  setResponseHeaders(event, {
    'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
    'Access-Control-Allow-Headers': 'Content-Type, Authorization',
  })

  if (getMethod(event) === 'OPTIONS') {
    event.node.res.statusCode = 204
    return ''
  }
})

Server middleware runs only on the server. It processes every request including API routes, static files, and SSR page renders. Use it for:

  • Logging all requests
  • CORS headers
  • Authentication token parsing (extracting the user from the token and attaching it to event context)
  • Request rate limiting at the infrastructure level

Plugins

Plugins are for initialization — running code once when the Nuxt application starts, either on the server or client. They register third-party libraries, set up global error handlers, configure API clients, and extend Vue.

// plugins/analytics.client.ts
export default defineNuxtPlugin(() => {
  // .client.ts suffix: runs only in the browser
  // Perfect for browser-only third-party libraries

  window.analytics = Analytics({
    app: 'my-app',
    version: '1.0.0',
    plugins: [segmentPlugin({ writeKey: useRuntimeConfig().public.segmentKey })],
  })
})
// plugins/sentry.ts
// No suffix: runs on both server and client
import * as Sentry from '@sentry/vue'

export default defineNuxtPlugin((nuxtApp) => {
  const config = useRuntimeConfig()

  Sentry.init({
    app: nuxtApp.vueApp,
    dsn: config.public.sentryDsn,
    environment: config.public.environment,
    integrations: [
      new Sentry.BrowserTracing({
        routingInstrumentation: Sentry.vueRouterInstrumentation(nuxtApp.$router),
      }),
    ],
    tracesSampleRate: 0.1,
  })
})

Plugins can provide helpers through the Nuxt app context:

// plugins/api.ts
export default defineNuxtPlugin(() => {
  const config = useRuntimeConfig()

  const api = $fetch.create({
    baseURL: config.public.apiBase,
    headers: {
      'Accept': 'application/json',
    },
    async onResponseError({ response }) {
      if (response.status === 401) {
        await navigateTo('/login')
      }
    },
  })

  return {
    provide: {
      api,
    },
  }
})

Anything provided through return { provide: { ... } } becomes available as useNuxtApp().$api or directly in templates as $api.

The Decision Tree

When you need to add some behavior to your Nuxt app, ask these questions:

Does it need to intercept page navigation? Use route middleware. Guard authenticated routes, redirect by role, track page views.

Does it need to process every HTTP request on the server? Use server middleware. Logging, CORS, auth token extraction.

Does it need to run once at startup to set something up? Use a plugin. Initialize analytics, configure a global API client, register a third-party library.

Is it logic that components share? Use a composable. Shared state, shared behavior, reusable reactive patterns.

Common Mistakes

Using a plugin to protect routes. Plugins run once at startup, not on every navigation. You cannot redirect users in a plugin based on authentication state. Use route middleware for that.

Using route middleware for API security. Route middleware can be bypassed by making direct API calls. Never use it as your only authentication check on API data. Protect API routes with server middleware or per-route authentication checks.

Using server middleware for client-only operations. Server middleware runs on the server, always. If you try to access window or localStorage in server middleware, it will throw an error.

Making route middleware async when it does not need to be. Every async middleware adds latency to navigation. Only await things you actually need before deciding whether to allow the navigation.

Plugin Execution Order

Plugins execute in the order they are listed in the plugins/ directory (alphabetically). When order matters, prefix filenames with numbers:

plugins/
  01.pinia.ts       ← First
  02.sentry.ts      ← Second (uses pinia state)
  03.analytics.ts   ← Third

Or specify order explicitly in nuxt.config.ts:

plugins: [
  '~/plugins/01.pinia.ts',
  '~/plugins/02.sentry.ts',
  '~/plugins/03.analytics.ts',
]

Middleware, plugins, and composables each have a clear purpose in Nuxt's architecture. The framework is opinionated about where logic goes, and following those opinions pays back in clarity and maintainability.


If you are working through a Nuxt architecture question or want a review of your middleware and plugin setup, I am happy to help. Book a call at calendly.com/jamesrossjr.


Keep Reading