Plugins
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 injectionx-tenant-slugheader 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)
})
})