Перейти к содержимому

15. Middleware

Middleware маршрутов позволяют выполнять код перед переходом на страницу. Это ключевой механизм для защиты маршрутов, редиректов, логирования навигации и проверки прав доступа.


Nuxt поддерживает три вида middleware:

middleware/
├── auth.ts # именованный middleware
├── admin.ts # именованный middleware
├── log.global.ts # глобальный (выполняется для всех страниц)
└── analytics.global.ts # глобальный

Подключаются явно через definePageMeta:

<script setup>
definePageMeta({
middleware: ['auth', 'admin']
})
</script>

Выполняются автоматически при каждой навигации:

middleware/log.global.ts
export default defineNuxtRouteMiddleware((to, from) => {
console.log(`Навигация: ${from.path} → ${to.path}`)
})

Определяются прямо в компоненте страницы:

<script setup>
definePageMeta({
middleware: defineNuxtRouteMiddleware((to, from) => {
// Логика прямо в компоненте
})
})
</script>

Самый распространённый паттерн — защита маршрутов:

middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
const user = useSupabaseUser()
// или
const { status } = useAuth()
if (!user.value) {
return navigateTo('/login', {
redirectCode: 301,
query: { redirect: to.fullPath }
})
}
})
pages/dashboard.vue
<script setup lang="ts">
definePageMeta({
middleware: 'auth'
})
</script>

navigateTo — утилита для программной навигации внутри middleware:

middleware/redirect.ts
export default defineNuxtRouteMiddleware((to, from) => {
// Простой редирект
if (to.path === '/old-path') {
return navigateTo('/new-path')
}
// Редирект с кодом статуса
if (to.path === '/moved') {
return navigateTo('https://new-site.com', {
external: true,
redirectCode: 301
})
}
// Редирект с сохранением query-параметров
if (!to.query.ref) {
return navigateTo({
path: to.path,
query: { ...to.query, ref: 'default' }
})
}
})

Останавливает навигацию без редиректа:

middleware/maintenance.ts
export default defineNuxtRouteMiddleware((to) => {
const config = useRuntimeConfig()
if (config.public.maintenanceMode) {
// Остановить навигацию с кодом ошибки
return abortNavigation({
statusCode: 503,
message: 'Сайт на техническом обслуживании'
})
}
})
middleware/access.ts
export default defineNuxtRouteMiddleware((to) => {
const user = useUser()
if (to.meta.requiresAdmin && !user.value?.isAdmin) {
// Просто отменить переход
return abortNavigation()
}
})

middleware/admin.ts
export default defineNuxtRouteMiddleware(async (to) => {
const { $auth } = useNuxtApp()
if (!$auth.isAuthenticated) {
return navigateTo('/login')
}
if (!$auth.user?.roles.includes('admin')) {
return navigateTo('/403')
}
})
middleware/permissions.ts
export default defineNuxtRouteMiddleware((to) => {
const userStore = useUserStore()
const requiredPermission = to.meta.permission as string
if (requiredPermission && !userStore.hasPermission(requiredPermission)) {
return abortNavigation({
statusCode: 403,
message: `Недостаточно прав: требуется "${requiredPermission}"`
})
}
})

Использование:

pages/users/manage.vue
<script setup lang="ts">
definePageMeta({
middleware: ['auth', 'permissions'],
permission: 'users:manage'
})
</script>

middleware/analytics.global.ts
export default defineNuxtRouteMiddleware((to, from) => {
// Отправка события в аналитику при каждом переходе
if (process.client) {
const { $gtag } = useNuxtApp()
$gtag?.('event', 'page_view', {
page_title: document.title,
page_path: to.fullPath
})
}
})
middleware/locale.global.ts
export default defineNuxtRouteMiddleware((to) => {
const { locale, availableLocales, defaultLocale } = useI18n()
// Определение языка из URL
const pathLocale = to.path.split('/')[1]
if (availableLocales.includes(pathLocale)) {
locale.value = pathLocale
} else {
locale.value = defaultLocale
}
})

Важно понимать порядок выполнения при нескольких middleware:

1. Глобальные middleware (в алфавитном порядке)
2. Именованные middleware (в порядке объявления в массиве)
3. Инлайн middleware
<script setup lang="ts">
definePageMeta({
// Порядок: сначала auth, потом admin
middleware: ['auth', 'admin']
})
</script>

В Nuxt 3 существуют два разных типа middleware:

Файлы в middleware/ — выполняются перед навигацией.

Файлы в server/middleware/ — выполняются для каждого HTTP-запроса:

server/middleware/cors.ts
export default defineEventHandler((event) => {
setResponseHeaders(event, {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE',
'Access-Control-Allow-Headers': 'Content-Type, Authorization'
})
if (getMethod(event) === 'OPTIONS') {
event.node.res.statusCode = 204
return null
}
})
server/middleware/auth.ts
export default defineEventHandler(async (event) => {
const token = getHeader(event, 'authorization')?.replace('Bearer ', '')
if (event.path.startsWith('/api/protected')) {
if (!token) {
throw createError({
statusCode: 401,
message: 'Требуется авторизация'
})
}
try {
const payload = verifyToken(token)
event.context.user = payload
} catch {
throw createError({
statusCode: 401,
message: 'Недействительный токен'
})
}
}
})

tests/middleware/auth.test.ts
import { describe, it, expect, vi } from 'vitest'
import { mockNuxtImport } from '@nuxt/test-utils/runtime'
const { useUser } = await mockNuxtImport('useUser', () => ({
useUser: vi.fn()
}))
describe('auth middleware', () => {
it('redirects unauthenticated users to /login', async () => {
useUser.mockReturnValue(ref(null))
const middleware = await import('~/middleware/auth')
const to = { path: '/dashboard', fullPath: '/dashboard' }
const from = { path: '/' }
const result = middleware.default(to, from)
expect(result).toEqual(expect.objectContaining({
path: '/login'
}))
})
})

ПаттернMiddlewareПрименение
Проверка авторизацииauth.tsЗащита всех приватных страниц
Проверка ролейadmin.tsРазграничение доступа
Редиректredirect.tsПереадресация устаревших URLs
Аналитикаanalytics.global.tsТрекинг всех переходов
Техобслуживаниеmaintenance.global.tsРежим обслуживания
Локальlocale.global.tsОпределение языка