Skip to content

Nuxt.js Integration ​

This guide covers how to integrate and use Pinia in Nuxt.js applications, including complete SSR support.

Installation ​

Using the Nuxt Module ​

Recommended approach using the official @pinia/nuxt module:

bash
npm install pinia @pinia/nuxt

Configure Nuxt ​

Add the module to your nuxt.config.ts:

ts
// nuxt.config.ts
export default defineNuxtConfig({
  modules: [
    '@pinia/nuxt',
  ],
  // Optional: Configure Pinia
  pinia: {
    autoImports: [
      // Automatically import `defineStore()`
      'defineStore',
      // Automatically import `defineStore()` as `definePiniaStore()`
      ['defineStore', 'definePiniaStore'],
    ],
  },
})

Basic Usage ​

Creating Stores ​

Create stores in the stores/ directory:

ts
// stores/user.ts
export const useUserStore = defineStore('user', () => {
  const user = ref(null)
  const isLoggedIn = computed(() => !!user.value)
  
  const login = async (credentials: LoginCredentials) => {
    const { data } = await $fetch('/api/auth/login', {
      method: 'POST',
      body: credentials
    })
    user.value = data.user
  }
  
  const logout = async () => {
    await $fetch('/api/auth/logout', { method: 'POST' })
    user.value = null
  }
  
  return {
    user: readonly(user),
    isLoggedIn,
    login,
    logout
  }
})

Using in Components ​

vue
<!-- pages/profile.vue -->
<template>
  <div>
    <div v-if="userStore.isLoggedIn">
      <h1>Welcome, {{ userStore.user.name }}!</h1>
      <button @click="userStore.logout">Logout</button>
    </div>
    <div v-else>
      <h1>Please Login</h1>
      <LoginForm @login="userStore.login" />
    </div>
  </div>
</template>

<script setup>
const userStore = useUserStore()

// Pre-fetch data on server
if (process.server && userStore.user) {
  await userStore.fetchUserProfile()
}
</script>

SSR Support ​

Server-side Data Fetching ​

Combine Nuxt's data fetching features with Pinia:

vue
<!-- pages/products/index.vue -->
<template>
  <div>
    <h1>Product List</h1>
    <div v-if="pending">Loading...</div>
    <div v-else>
      <ProductCard 
        v-for="product in productStore.products" 
        :key="product.id" 
        :product="product" 
      />
    </div>
  </div>
</template>

<script setup>
const productStore = useProductStore()

// Use useLazyAsyncData for data pre-fetching
const { pending } = await useLazyAsyncData('products', async () => {
  if (!productStore.products.length) {
    await productStore.fetchProducts()
  }
})

// Or use refresh method
const refresh = () => productStore.fetchProducts()
</script>

State Hydration ​

Pinia automatically handles server-to-client state hydration:

ts
// stores/app.ts
export const useAppStore = defineStore('app', () => {
  const settings = ref({
    theme: 'light',
    language: 'en-US'
  })
  
  const initializeApp = async () => {
    // This method runs on both server and client
    if (process.server) {
      // Server-side initialization logic
      const config = await $fetch('/api/config')
      settings.value = { ...settings.value, ...config }
    } else {
      // Client-side initialization logic
      const savedSettings = localStorage.getItem('app-settings')
      if (savedSettings) {
        settings.value = { ...settings.value, ...JSON.parse(savedSettings) }
      }
    }
  }
  
  return {
    settings,
    initializeApp
  }
})

Plugin Integration ​

Creating Pinia Plugins ​

ts
// plugins/pinia.client.ts
export default defineNuxtPlugin(({ $pinia }) => {
  $pinia.use(({ store }) => {
    // Client-only plugin logic
    if (process.client) {
      // Add persistence
      const savedState = localStorage.getItem(`pinia-${store.$id}`)
      if (savedState) {
        store.$patch(JSON.parse(savedState))
      }
      
      // Watch changes and save
      store.$subscribe((mutation, state) => {
        localStorage.setItem(`pinia-${store.$id}`, JSON.stringify(state))
      })
    }
  })
})

Server-side Plugins ​

ts
// plugins/pinia.server.ts
export default defineNuxtPlugin(({ $pinia }) => {
  $pinia.use(({ store }) => {
    // Server-specific plugin logic
    if (process.server) {
      // Add server-side logging
      store.$onAction(({ name, args }) => {
        console.log(`[Server] Action ${name} called with:`, args)
      })
    }
  })
})

Middleware Integration ​

Route Middleware ​

ts
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to) => {
  const userStore = useUserStore()
  
  if (!userStore.isLoggedIn) {
    return navigateTo('/login')
  }
})

Global Middleware ​

ts
// middleware/app.global.ts
export default defineNuxtRouteMiddleware(async (to) => {
  const appStore = useAppStore()
  
  // Ensure app is initialized
  if (!appStore.isInitialized) {
    await appStore.initialize()
  }
  
  // Set page title based on route
  if (to.meta.title) {
    appStore.setPageTitle(to.meta.title)
  }
})

API Integration ​

Using $fetch ​

ts
// stores/api.ts
export const useApiStore = defineStore('api', () => {
  const loading = ref(false)
  const error = ref(null)
  
  const apiCall = async <T>(url: string, options: any = {}): Promise<T> => {
    loading.value = true
    error.value = null
    
    try {
      const data = await $fetch<T>(url, {
        ...options,
        onRequest({ request, options }) {
          // Add auth headers
          const userStore = useUserStore()
          if (userStore.token) {
            options.headers = {
              ...options.headers,
              Authorization: `Bearer ${userStore.token}`
            }
          }
        },
        onResponseError({ response }) {
          // Handle auth errors
          if (response.status === 401) {
            const userStore = useUserStore()
            userStore.logout()
            navigateTo('/login')
          }
        }
      })
      
      return data
    } catch (err) {
      error.value = err
      throw err
    } finally {
      loading.value = false
    }
  }
  
  return {
    loading: readonly(loading),
    error: readonly(error),
    apiCall
  }
})

Reactive API Calls ​

ts
// composables/useApi.ts
export const useApi = <T>(url: string, options: any = {}) => {
  const apiStore = useApiStore()
  
  return useLazyAsyncData<T>(url, () => 
    apiStore.apiCall<T>(url, options)
  )
}

// Usage in components
const { data: products, pending, error, refresh } = await useApi('/api/products')

Type Safety ​

Store Type Definitions ​

ts
// types/stores.ts
export interface User {
  id: string
  name: string
  email: string
  role: 'admin' | 'user'
}

export interface UserState {
  user: User | null
  token: string | null
  preferences: UserPreferences
}

export interface UserPreferences {
  theme: 'light' | 'dark'
  language: string
  notifications: boolean
}

Typed Stores ​

ts
// stores/user.ts
import type { User, UserState, UserPreferences } from '~/types/stores'

export const useUserStore = defineStore('user', (): UserState & {
  // Actions
  login(credentials: LoginCredentials): Promise<void>
  logout(): Promise<void>
  updatePreferences(prefs: Partial<UserPreferences>): void
  
  // Getters
  isLoggedIn: ComputedRef<boolean>
  isAdmin: ComputedRef<boolean>
} => {
  const user = ref<User | null>(null)
  const token = ref<string | null>(null)
  const preferences = ref<UserPreferences>({
    theme: 'light',
    language: 'en-US',
    notifications: true
  })
  
  const isLoggedIn = computed(() => !!user.value && !!token.value)
  const isAdmin = computed(() => user.value?.role === 'admin')
  
  const login = async (credentials: LoginCredentials) => {
    const response = await $fetch<{ user: User; token: string }>('/api/auth/login', {
      method: 'POST',
      body: credentials
    })
    
    user.value = response.user
    token.value = response.token
  }
  
  const logout = async () => {
    await $fetch('/api/auth/logout', { method: 'POST' })
    user.value = null
    token.value = null
  }
  
  const updatePreferences = (prefs: Partial<UserPreferences>) => {
    preferences.value = { ...preferences.value, ...prefs }
  }
  
  return {
    user: readonly(user),
    token: readonly(token),
    preferences,
    isLoggedIn,
    isAdmin,
    login,
    logout,
    updatePreferences
  }
})

Testing ​

Unit Testing ​

ts
// tests/stores/user.test.ts
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import { useUserStore } from '~/stores/user'

// Mock $fetch
vi.mock('#app', () => ({
  $fetch: vi.fn()
}))

describe('useUserStore', () => {
  beforeEach(() => {
    setActivePinia(createPinia())
    vi.clearAllMocks()
  })
  
  it('should handle login correctly', async () => {
    const store = useUserStore()
    const mockUser = { id: '1', name: 'John', email: 'john@example.com', role: 'user' }
    const mockToken = 'mock-token'
    
    // Mock API response
    vi.mocked($fetch).mockResolvedValueOnce({
      user: mockUser,
      token: mockToken
    })
    
    await store.login({ email: 'john@example.com', password: 'password' })
    
    expect(store.user).toEqual(mockUser)
    expect(store.token).toBe(mockToken)
    expect(store.isLoggedIn).toBe(true)
  })
  
  it('should handle logout correctly', async () => {
    const store = useUserStore()
    
    // Set initial state
    store.$patch({
      user: { id: '1', name: 'John', email: 'john@example.com', role: 'user' },
      token: 'mock-token'
    })
    
    vi.mocked($fetch).mockResolvedValueOnce({})
    
    await store.logout()
    
    expect(store.user).toBeNull()
    expect(store.token).toBeNull()
    expect(store.isLoggedIn).toBe(false)
  })
})

Integration Testing ​

ts
// tests/integration/auth.test.ts
import { describe, it, expect } from 'vitest'
import { mountSuspended } from '@nuxt/test-utils/runtime'
import LoginPage from '~/pages/login.vue'

describe('Authentication Integration Tests', () => {
  it('should render login page correctly', async () => {
    const component = await mountSuspended(LoginPage)
    
    expect(component.text()).toContain('Login')
  })
  
  it('should handle login flow', async () => {
    const component = await mountSuspended(LoginPage)
    
    // Simulate user input
    await component.find('input[type="email"]').setValue('test@example.com')
    await component.find('input[type="password"]').setValue('password')
    
    // Submit form
    await component.find('form').trigger('submit')
    
    // Verify state changes
    const userStore = useUserStore()
    expect(userStore.isLoggedIn).toBe(true)
  })
})

Performance Optimization ​

Lazy Loading Stores ​

ts
// composables/useLazyStore.ts
export const useLazyStore = <T>(storeFactory: () => T): Promise<T> => {
  return new Promise((resolve) => {
    if (process.client) {
      // Client-side lazy loading
      nextTick(() => {
        resolve(storeFactory())
      })
    } else {
      // Server-side immediate loading
      resolve(storeFactory())
    }
  })
}

// Usage example
const store = await useLazyStore(() => useHeavyStore())

Code Splitting ​

ts
// stores/index.ts
export const useUserStore = () => import('./user').then(m => m.useUserStore)
export const useProductStore = () => import('./product').then(m => m.useProductStore)
export const useCartStore = () => import('./cart').then(m => m.useCartStore)

// Usage in components
const loadUserStore = async () => {
  const { useUserStore } = await import('~/stores/user')
  return useUserStore()
}

Deployment Considerations ​

Environment Variables ​

ts
// nuxt.config.ts
export default defineNuxtConfig({
  runtimeConfig: {
    // Server-side environment variables
    apiSecret: process.env.API_SECRET,
    
    public: {
      // Client-side environment variables
      apiBase: process.env.NUXT_PUBLIC_API_BASE || '/api'
    }
  }
})

// Usage in stores
const config = useRuntimeConfig()
const apiBase = config.public.apiBase

Production Optimization ​

ts
// nuxt.config.ts
export default defineNuxtConfig({
  nitro: {
    // Pre-render routes
    prerender: {
      routes: ['/sitemap.xml']
    }
  },
  
  // Build optimization
  build: {
    transpile: ['pinia']
  },
  
  // Experimental features
  experimental: {
    payloadExtraction: false // If using large amounts of store data
  }
})

Best Practices ​

Store Organization ​

stores/
├── index.ts          # Store exports
├── user.ts           # User-related
├── auth.ts           # Authentication-related
├── products/         # Product module
│   ├── index.ts
│   ├── list.ts
│   └── detail.ts
└── utils/            # Store utilities
    ├── api.ts
    └── cache.ts

Error Handling ​

ts
// stores/error.ts
export const useErrorStore = defineStore('error', () => {
  const errors = ref<Array<AppError>>([])
  
  const addError = (error: AppError) => {
    errors.value.push(error)
    
    // Auto-clear errors
    setTimeout(() => {
      removeError(error.id)
    }, 5000)
  }
  
  const removeError = (id: string) => {
    const index = errors.value.findIndex(e => e.id === id)
    if (index > -1) {
      errors.value.splice(index, 1)
    }
  }
  
  const clearErrors = () => {
    errors.value = []
  }
  
  return {
    errors: readonly(errors),
    addError,
    removeError,
    clearErrors
  }
})

// Global error handling
export default defineNuxtPlugin(() => {
  const errorStore = useErrorStore()
  
  // Catch unhandled errors
  if (process.client) {
    window.addEventListener('unhandledrejection', (event) => {
      errorStore.addError({
        id: Date.now().toString(),
        message: event.reason.message || 'Unknown error',
        type: 'error'
      })
    })
  }
})

Development Tools ​

ts
// plugins/devtools.client.ts
export default defineNuxtPlugin(() => {
  if (process.dev) {
    // Development environment debugging tools
    window.__PINIA_STORES__ = {}
    
    const pinia = usePinia()
    pinia.use(({ store }) => {
      window.__PINIA_STORES__[store.$id] = store
    })
  }
})

Common Questions ​

Q: How to use Pinia in Nuxt 3? ​

A: Use the @pinia/nuxt module, which provides complete Nuxt 3 support including auto-imports and SSR.

Q: What if state is inconsistent between server and client? ​

A: Ensure you use the same data fetching logic on both server and client, and properly handle async operations.

Q: How to persist store state in Nuxt? ​

A: Use plugins to save state to localStorage on the client, and restore from cookies or database on the server.

Q: How to handle authentication state? ​

A: Use middleware to check auth state, restore user info from cookies on server, and listen for auth changes on client.

Released under the MIT License.