Frontend

Middleware

Route middleware conventions — named, global, and role-based.

Types

TypeFilename patternRuns on
Namedauth.ts, admin.tsPages that declare it in definePageMeta
Globaltenant.global.tsEvery route navigation automatically
Ordered global01.org.tsEvery 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) return to 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.