Nuxt Guide
Middleware
Named, global, and ordered route middleware conventions.
Types
| Type | Filename pattern | Runs on |
|---|---|---|
| Named | auth.ts, admin.ts | Pages that declare it in definePageMeta |
| Global | tenant.global.ts | Every route navigation automatically |
| Ordered global | 01.org.ts | Every route — number controls execution order |
Client-Only Guard
All middleware should be client-only in SPA apps. Add this guard at the top when middleware accesses cookies or browser APIs:
export default defineNuxtRouteMiddleware(() => {
if (import.meta.server) return
// ...
})
Named — auth.ts
Redirects unauthenticated users to /login. Used on all protected pages.
// app/middleware/auth.ts
export default defineNuxtRouteMiddleware(async (to) => {
if (import.meta.server) return
const publicRoutes = ['/login', '/register', '/']
if (publicRoutes.includes(to.path)) return
const accessToken = useCookie('accessToken')
if (!accessToken?.value) return navigateTo('/login')
})
Applied in a page:
definePageMeta({ middleware: ['auth'] })
Named — admin.ts
Throws a 403 error if the current user is not an admin:
// app/middleware/admin.ts
export default defineNuxtRouteMiddleware(() => {
if (import.meta.server) return
const { currentUser } = useAuth()
if (!currentUser.value || currentUser.value.type !== 'ADMIN') {
throw createError({
statusCode: 403,
statusMessage: 'Access Denied: Admin privileges required',
})
}
})
Global — tenant.global.ts
Runs on every route. Resolves the tenant from subdomains and stores in global state. Sets a tenantNotFound flag if the org cannot be resolved.
// app/middleware/tenant.global.ts
export default defineNuxtRouteMiddleware(async () => {
if (import.meta.server) return
const tenant = useTenant()
const orgState = useState<any | null>('currentOrganization', () => null)
const tenantNotFound = useState<boolean>('tenantNotFound', () => false)
// Already loaded — skip
if (orgState.value) {
tenantNotFound.value = false
return
}
if (!tenant.value?.slug) {
tenantNotFound.value = true
return
}
try {
const res = await useOrganization().getByTenant()
orgState.value = res.organization
tenantNotFound.value = false
} catch (e: any) {
if (e?.status === 404) tenantNotFound.value = true
}
})
Ordered Global — 01.org.ts
Numeric prefix controls execution order (lower numbers run first). Validates IDs and loads org into state.
// app/middleware/01.org.ts
import { z } from 'zod'
const hexSchema = z.string().regex(/^[0-9a-fA-F]{24}$/, 'Invalid ID')
export default defineNuxtRouteMiddleware(async (to) => {
if (import.meta.server) return
const auth = useAuth()
const orgApi = useOrganization()
const orgState = useState<any | null>('currentOrganization', () => null)
if (!auth.currentUser.value) {
await auth.getCurrentUser()
}
const orgId = to.params.organization || auth.currentUser.value?.organizationId
const parsed = hexSchema.safeParse(orgId)
if (!parsed.success) return navigateTo('/organizations')
const response = await orgApi.getById(orgId as string)
orgState.value = response
})
Summary of Rules
- Guard every middleware with
if (import.meta.server) return. - Use
navigateTo()to redirect;throw createError()for error pages. - Prefer named middleware over global unless the check truly needs to run on every route.
- Use numeric prefixes (
01.,02.) when multiple global middlewares need a defined execution order.