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.