Frontend
Middleware
Route middleware conventions — named, global, and role-based.
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 the 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) => {
const publicRoutes = ['/login', '/register', '/reserve', '/']
const publicPatterns = [/^\/verify-booking\/.+$/]
const isPublic =
publicRoutes.includes(to.path) ||
publicPatterns.some((p) => p.test(to.path))
if (isPublic) 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. Used only in admin-booki-web-app.
// app/middleware/admin.ts
export default defineNuxtRouteMiddleware(() => {
if (import.meta.server) return
const { currentUser, isAdmin } = useAuth()
if (!currentUser.value || !isAdmin.value) {
throw createError({
statusCode: 403,
statusMessage: 'Access Denied: Admin privileges required',
})
}
})
Global — tenant.global.ts
Runs on every route in customer-booki-web-app. Resolves the tenant org from the subdomain and stores it in global state. Sets tenantNotFound if no org is found.
// 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 orgApi = useOrganization()
const res = await orgApi.getOrganizationByTenant()
orgState.value = res.organization
tenantNotFound.value = false
} catch (e: any) {
if (e?.status === 404 || e?.data?.message?.includes('not found')) {
tenantNotFound.value = true
}
}
})
Ordered Global — 01.org.ts
Uses a numeric prefix to control execution order (lower numbers run first). Validates the org ID from the route and loads the org into state.
// app/middleware/01.org.ts (owner-booki-web-app)
import { z } from 'zod'
const hexSchema = z.string().regex(/^[0-9a-fA-F]{24}$/, 'Invalid organization ID')
export default defineNuxtRouteMiddleware(async (to) => {
if (import.meta.server) return
const auth = useAuth()
const orgApi = useOrganization()
const orgState = useState<any | null>('currentOrganization', () => null)
// Lazy-load current user if missing
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.getOrganizationById(orgId as string)
orgState.value = response
})
Summary of Rules
- Guard every middleware with
if (import.meta.server) returnto avoid running on the server. - 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.