Nuxt Guide

Plugins

The $api plugin, auth bootstrap, Socket.IO, and icon registration.

All shared plugins live in app/plugins/ (or in a shared Nuxt layer) and are auto-loaded.

api.ts — Universal HTTP Client

The most important plugin. Creates a typed $fetch instance pre-configured with:

  • Authorization: Bearer <token> header injection
  • x-tenant-slug header for multi-tenant requests
  • Automatic 401 → logout redirect
// app/plugins/api.ts
export default defineNuxtPlugin(() => {
  const api = $fetch.create({
    baseURL: '/',
    retry: 1,
    retryStatusCodes: [401],
    retryDelay: 500,

    onRequest({ options }) {
      const token = useCookie<string | null>('accessToken').value
      options.headers = new Headers(options.headers)
      if (token) options.headers.set('Authorization', `Bearer ${token}`)

      // Multi-tenant: inject slug header on every request
      const tenant = useTenant()
      if (tenant?.value?.slug) {
        options.headers.set('x-tenant-slug', tenant.value.slug)
      }
    },

    async onResponseError({ response }) {
      if (response.status === 401) {
        useCookie<string | null>('accessToken').value = null
        useState<any | null>('currentUser').value     = null
        await navigateTo('/login')
      }
    },
  })

  return { provide: { api } }   // → useNuxtApp().$api
})

Usage in a composable:

function getAll() {
  return useNuxtApp().$api<{ items: IResource[]; pages: number }>('/api/resources', {
    method: 'GET',
    query: { page: 1, limit: 10 },
  })
}

Never call $fetch directly in app code — always use $api so headers are applied consistently.

If there is no multi-tenancy, remove the tenant header injection.

auth.client.ts — Bootstrap User on Mount

Runs once when the app loads. If an access token cookie exists, fetches the current user silently. Clears the token if the request fails.

// app/plugins/auth.client.ts
export default defineNuxtPlugin(async () => {
  const { getCurrentUser } = useAuth()
  const token = useCookie<string | null>('accessToken')

  if (token.value) {
    try {
      await getCurrentUser()
    } catch {
      token.value = null    // clear invalid token silently
    }
  }
})

The .client.ts suffix ensures this plugin only runs in the browser, never during SSR.

socket.client.ts — Socket.IO

Provides a $socket object with typed room helpers.

// app/plugins/socket.client.ts
export default defineNuxtPlugin(() => {
  if (import.meta.server) return { provide: { socket: null } }

  const config = useRuntimeConfig()
  const { token } = useAuth()
  let socket: Socket | null = null

  const connect = () => {
    socket = io(config.public.socketUrl, {
      auth:        { token: token.value || undefined },
      transports:  ['polling', 'websocket'],
      autoConnect: false,
      reconnection: true,
    })
    return socket
  }

  return {
    provide: {
      socket: {
        connect,
        joinRoom:    (roomId: string) => socket?.emit('join:room', roomId),
        on:  (...args: Parameters<Socket['on']>)  => socket?.on(...args),
        off: (...args: Parameters<Socket['off']>) => socket?.off(...args),
      },
    },
  }
})

Usage in a composable or component:

const { $socket } = useNuxtApp()

onMounted(() => {
  $socket?.on('resource:updated', (data) => {
    // handle real-time update
  })
})

onUnmounted(() => {
  $socket?.off('resource:updated')
})

phosphor-icons.ts — Global Icons

Registers Phosphor icon components globally so they don't need to be imported per-component. Add new icons here when needed:

// app/plugins/phosphor-icons.ts
import {
  PhEye, PhCaretLeft, PhMagnifyingGlass, PhList,
  PhPencilSimple, PhTrash, PhPlus, PhX, PhCheck,
} from '@phosphor-icons/vue'

export default defineNuxtPlugin((nuxtApp) => {
  const icons: Record<string, Component> = {
    PhEye, PhCaretLeft, PhMagnifyingGlass, PhList,
    PhPencilSimple, PhTrash, PhPlus, PhX, PhCheck,
  }
  Object.entries(icons).forEach(([name, component]) => {
    nuxtApp.vueApp.component(name, component)
  })
})