Streamline Vue Apps with Nuxt-Style Middleware in Vite
Navigating through complex Vue apps can sometimes become cumbersome, especially when dealing with navigation guards. This becomes evident when your application requires more than basic authentication checks, such as analytics, permission management, feature flags, or prefetching. Traditionally, these logics pile up in the beforeEach hook, leading to cluttered and hard-to-maintain code.
Existing Solutions
Several community solutions attempt to address these challenges:
- Custom Middleware Runners: Although functional, this approach involves reinventing the wheel for each project without type safety.
- Vue-Router-Middleware-Plugin: While it is a close alternative, it lacks TypeScript support for middleware names and asynchronous context handling.
- Nuxt: This full SSR framework is sometimes overkill for a simple SPA.
To bridge these gaps, a new solution emerges: vite-plugin-vue-middleware. This plugin aims to integrate middleware into Vue SPA as naturally as it does in Nuxt, all while staying within the Vite environment.
Key Challenges and Solutions
Problem 1: Overloaded beforeEach
Solution: Each middleware is defined in its own file within src/middleware/. The plugin scans this directory automatically, organizing and linking the middleware seamlessly.
src/middleware/
├── 01.auth.global.ts ← runs on every route, first
├── 02.log.global.ts ← runs on every route, second
├── admin.ts ← only runs when the route opts in
└── guest.ts
Router setup transforms into a single line:
import { setupMiddleware } from 'virtual:vue-middleware'
setupMiddleware(router)
Problem 2: Lack of Type Safety for Middleware Names
Solution: The plugin generates a .d.ts file that enhances vue-router's RouteMeta, providing full type safety and IntelliSense for middleware names.
definePage({
meta: {
middleware: ['admin', 'guest'] // Autocomplete and error-checking
},
})
Problem 3: inject() Failing Post-await
Vue's inject() fails in an async context after await, causing runtime errors. This is particularly problematic when using TanStack Vue Query as it relies on inject() internally.
Solution: The plugin employs a build-time AST transformation, converting async middleware into a generator-based executor. This ensures the Vue injection context is preserved after each await.
// Works seamlessly with injection available after every await
export default defineMiddleware(async (to) => {
await validateSession()
const queryClient = useQueryClient() // Available now
await queryClient.prefetchQuery(userQueryOptions)
})
Quick Start Guide
-
Install the Plugin
npm install -D vite-plugin-vue-middleware -
Configure Vite
// vite.config.ts import vueMiddleware from 'vite-plugin-vue-middleware' export default defineConfig({ plugins: [vue(), vueMiddleware()], }) -
Setup Middleware in Router
// src/router/index.ts import { setupMiddleware } from 'virtual:vue-middleware' setupMiddleware(router) -
Create Middleware Files
// src/middleware/01.auth.global.ts import { defineMiddleware } from 'virtual:vue-middleware' export default defineMiddleware(async (to) => { const isLoggedIn = await checkAuth() if (!isLoggedIn && to.path !== '/login') return '/login' })
File Naming Conventions
- auth.global.ts: Executes on every route navigation
- 01.auth.global.ts: Global middleware with execution order controlled by a numeric prefix
- admin.ts: Executes only when
meta.middlewareincludes 'admin'
Compatibility with unplugin-vue-router
For those using file-based routing with unplugin-vue-router, middleware can be declared directly within .vue files:
<script setup lang="ts">
definePage({
meta: {
middleware: ['auth', 'admin'],
},
})
</script>
This approach includes full type safety.
Get Involved
Explore the project on GitHub or npm. Feedback and contributions are welcome, particularly if you've encountered the async context issue.
A star on GitHub would greatly help in bringing this project to a wider audience.