Skip to content
On this page
v2.0.0-pre.18

Nuxt 3

For the TLDR (Too Long, Didn't Read) version, you can take a look at the feathers-pinia-nuxt3 repo. For now, the app currently only provides a demo of authentication. More features will be demonstrated at a future time.

Overview

Follow these steps to get started with a new Nuxt app:

  1. Create a Nuxt app
  2. Install Feathers-Pinia,
  3. Follow the instructions, below.

tip

Note that for auto-import to work in Nuxt 3, the dev server must be running. The dev server builds the TypeScript types for you as you code, which is really convenient.

1. Feathers Client

In Nuxt, we setup the Feathers Client in a Nuxt plugin. This way, every request has its own client instance, preventing the ability to leak data between requests.

Nuxt supports Static Site Generation (SSG), Server-Side Rendering (SSR), and Hybrid Rendering (mixed rendering types). The primary difference between setting them up involves the Feathers Client. This example will use @feathersjs/rest with fetch on the server and @feathersjs/socketio in the browser.

Since we need an SSR-compatible version of fetch, we will use ofetch.

bash
npm i ofetch -D

Next, create a file named 1.feathers.ts in the plugins folder. We prefix with a 1 because Nuxt plugins are run in alphabetical order. We want Feathers to load before other plugins that might use it.

ts
// plugins/1.feathers.ts
import { createClient } from 'feathers-pinia-api'

// rest imports for the server
import { $fetch } from 'ofetch'
import rest from '@feathersjs/rest-client'
import { OFetch } from 'feathers-pinia'

// socket.io imports for the browser
import socketio from '@feathersjs/socketio-client'
import io from 'socket.io-client'

/**
 * Creates a Feathers Rest client for the SSR server and a Socket.io client for the browser.
 * Also provides a cookie-storage adapter for JWT SSR using Nuxt APIs.
 */
export default defineNuxtPlugin(async (_nuxtApp) => {
  const host = import.meta.env.VITE_MYAPP_API_URL as string || 'http://localhost:3030'

  // Store JWT in a cookie for SSR.
  const storageKey = 'feathers-jwt'
  const jwt = useCookie<string | null>(storageKey)
  const storage = {
    getItem: () => jwt.value,
    setItem: (key: string, val: string) => (jwt.value = val),
    removeItem: () => (jwt.value = null),
  }

  // Use Rest for the SSR Server and socket.io for the browser
  const connection = process.server
    ? rest(host).fetch($fetch, OFetch)
    : socketio(io(host, { transports: ['websocket'] }))

  // create the api client
  const api = createClient(connection, { storage, storageKey })

  return { provide: { api } }
})

The previous code snippet utilizes Nuxt's useCookie for SSR compatibility. If you plan to use SSG or a non-server-based rendering strategy, see SSG-Compatible localStorage on the Common Patterns page.

Also, notice the line at the end: return { provide: { api } }. This line makes the api available to the rest of the Nuxt application. We'll use it after we setup Pinia.

2. Install Pinia

Let's get Pinia installed and update the Nuxt config:

bash
npm install pinia @pinia/nuxt

Setup your Nuxt config:

ts
// nuxt.config.ts
// https://v3.nuxtjs.org/api/configuration/nuxt.config
export default defineNuxtConfig({
  modules: [
    '@pinia/nuxt',
    'nuxt-feathers-pinia',
  ],
  // Allows you to put stores and models in their own folders
  imports: {
    dirs: [
      'stores',
      'models',
    ],
  },
  // Enable Nuxt Takeover Mode
  typescript: {
    shim: false,
  },
  // optional, Vue Reactivity Transform
  experimental: {
    reactivityTransform: true,
  },
})

You can read more about the above configuration at these links:

Using the extra imports as shown above enables Nuxt's auto-import feature in the models and stores folders, which will come in handy for Model and store creation, later.

Finally, if npm is your package manager and you see the error ERESOLVE unable to resolve dependency tree, add this to your package.json:

json
"overrides": { 
  "vue": "latest"
}

3. Composable Utils

3.1 useFeathers Client Access

Let's create a composable that gives us instant access to the Feathers Client.

ts
// composables/feathers.ts

// Provides access to Feathers clients
export const useFeathers = () => {
  const { $api: api } = useNuxtApp()
  return { api }
}

Any key returned in a Nuxt plugin's provide object will have a $ prepended. The useFeathers composables pulls the $api object from useNuxtApp, renames it to api and returns it in another object. You could return multiple clients in this same object. With the above composable in place, we can now access the Feathers client from within in components, plugins, and middleware:

ts
const { api } = useFeathers()

Auto-imports decouple our code from module paths and are super convenient. Read more about Auto-Imports in the Nuxt Module.

3.2 Global Config

Next let's create a global configuration composable, which will give access to the pinia instance and other options which are shared between services.

ts
// src/models/feathers-pinia-config.ts
/**
 * Returns a global configuration object for Feathers-Pinia
 */
export const useFeathersPiniaConfig = () => {
  const { $pinia: pinia } = useNuxtApp()
  return {
    pinia,
    idField: '_id',
    whitelist: ['$regex'],
  }
}

The above composable gives us two benefits:

  1. DRY code: changing a shared value in one place updates it for all services.
  2. Quick access to the values from anywhere in the app.

4. Model Classes

You're ready to begin Data Modeling. Feathers-Pinia can directly use TypeScript types from a FeathersJS v5 Dove backend, or you can provide your own types. Let's create two Model Functions: User and Task.

4.1. User Model

Here's the User Model. Notice that since Feathers-Pinia v2 is highly modular, using Auto-Imports really cleans things up.

Note about Feathers Types

Replace my-feathers-api in the below example with the package installed from your Feathers v5 Dove API. You can also provide manual types that describe the shape of your data.

ts
import type { ModelInstance } from 'feathers-pinia'
import type { User, UserData, UserQuery } from 'feathers-pinia-api'

export const useUsersConfig = () => {
  const { pinia, idField, whitelist } = useFeathersPiniaConfig()
  const servicePath = 'users'
  const service = useFeathersService<User, UserQuery>(servicePath)
  const name = 'User'

  return { pinia, idField, whitelist, servicePath, service, name }
}

export const useUserModel = () => {
  const { idField, service, name } = useUsersConfig()

  const Model = useModel(name, () => {
    const modelFn = (data: ModelInstance<User>) => {
      const defaults = {
        email: '',
        password: '',
      }
      const withDefaults = useInstanceDefaults(defaults, data)
      return withDefaults
    }
    return useFeathersModel<User, UserData, UserQuery, typeof modelFn>({ name, idField, service }, modelFn)
  })

  onModelReady(name, () => {
    service.hooks({ around: { all: [...feathersPiniaHooks(Model)] } })
  })
  connectModel(name, () => Model, useUserStore)

  return Model
}
ts
import {
  type ModelInstance,
  useModel,
  associateFind,
  feathersPiniaHooks,
  useFeathersModel,
  useInstanceDefaults,
  onModelReady,
  connectModel,
} from 'feathers-pinia'
import { useFeathersPiniaConfig } from '../feathers-pinia-config'
import type { User, UserData, UserQuery } from 'feathers-pinia-api'

export const useUsersConfig = () => {
  const { pinia, idField, whitelist } = useFeathersPiniaConfig()
  const servicePath = 'users'
  const service = useFeathersService<User, UserQuery>(servicePath)
  const name = 'User'

  return { pinia, idField, whitelist, servicePath, service, name }
}

export const useUserModel = () => {
  const { idField, service, name } = useUsersConfig()

  const Model = useModel(name, () => {
    const modelFn = (data: ModelInstance<User>) => {
      const defaults = {
        email: '',
        password: '',
      }
      const withDefaults = useInstanceDefaults(defaults, data)
      return withDefaults
    }
    return useFeathersModel<User, UserData, UserQuery, typeof modelFn>({ name, idField, service }, modelFn)
  })

  onModelReady(name, () => {
    service.hooks({ around: { all: [...feathersPiniaHooks(Model)] } })
  })
  connectModel(name, () => Model, useUserStore)

  return Model
}

This code does more than setup the Model. It also

  • assures the Model is only created once per request, even if you call useUserModel multiple times.
  • allows the Model and store to be kept in different folders, keeping Models in models and stores in stores.
  • assures the Model and store are properly connected.
  • assures hooks are only registered once.

Model.store vs store

Models have a store property that references the pinia store. (We will setup the pinia stores in the next steps) The current types won't pick up on customizations. This means that for customized stores, you'll need to access them with their own useUserStore or equivalent function.

In this tutorial, User.store and useUserStore() both hold the same value, but TypeScript doesn't know it, yet.

This limitation will be fixed in a future release.

4.2. Task Model

Now let's create the Task Model:

ts
import type { ModelInstance } from 'feathers-pinia'
import type { Tasks, TasksData, TasksQuery } from 'feathers-pinia-api'

export const useTasksConfig = () => {
  const { pinia, idField, whitelist } = useFeathersPiniaConfig()
  const servicePath = 'tasks'
  const service = useFeathersService<Tasks, TasksQuery>(servicePath)
  const name = 'Task'

  return { pinia, idField, whitelist, servicePath, service, name }
}

export const useTaskModel = () => {
  const { idField, service, name } = useTasksConfig()

  const Model = useModel(name, () => {
    const modelFn = (data: ModelInstance<Tasks>) => {
      const defaults = {
        description: '',
        isComplete: false,
      }
      const withDefaults = useInstanceDefaults(defaults, data)
      return withDefaults
    }
    return useFeathersModel<Tasks, TasksData, TasksQuery, typeof modelFn>({ name, idField, service }, modelFn)
  })

  onModelReady(name, () => {
    service.hooks({ around: { all: [...feathersPiniaHooks(Model)] } })
  })
  connectModel(name, () => Model, useTaskStore)

  return Model
}

Since we wrapped our Models in utility functions, we can use them with auto-imports just like any utility in composables:

vue
<script setup lang="ts">
const User = useUserModel()
const Task = useTaskModel()
</script>

5. Service Stores

In Nuxt 3, the user stores are setup as auto-imported composables, making them really convenient to use. (If you haven't noticed, yet, one of the primary themes of Nuxt 3 is convenient Developer Experience.)

5.1 Users Service

To setup the /users service store, create the following file:

ts
// composables/service.users.ts
import { defineStore } from 'pinia'
import { useService } from 'feathers-pinia'

export const useUserStore = () => {
  const { pinia, idField, whitelist, servicePath, service, name } = useUsersConfig()

  const useStore = defineStore(servicePath, () => {
    const utils = useService({ service, idField, whitelist })
    return { ...utils }
  })
  const store = useStore(pinia)

  connectModel(name, useUserModel, () => store)

  return store
}

With the above file in place, you can call const userStore = useUserStore() from any component to get the userStore.

5.2 Tasks Service

To setup the /tasks service store, create the following file:

ts
// composables/service.tasks.ts
import { defineStore } from 'pinia'
import { useService } from 'feathers-pinia'

export const useTaskStore = () => {
  const { pinia, idField, whitelist, servicePath, service, name } = useTasksConfig()

  const useStore = defineStore(servicePath, () => {
    const utils = useService({ service, idField, whitelist })
    return { ...utils }
  })
  const store = useStore(pinia)

  connectModel(name, useTaskModel, () => store)

  return store
}

Now we can use the taskStore by calling const taskStore = useTaskStore().

6. Authentication

If your app requires user login, the following sections demonstrate how to implement it.

Assess Your Risk

The auth examples on this page will suffice for apps with simple security requirements. If you are building an app with privacy requirements, you need something more secure.

There are multiple ways to secure your app. If you need help, please contact a FeathersHQ member for consulting services.

6.1 Auth Store

Feathers-Pinia 2.0 uses a setup store for the auth store. The new useAuth utility contains all of the logic for authentication in most apps. Using the composition API allows more simplicity and more flexibility for custom scenarios. We'll keep this example simple. To implement auth, create the file below:

Note about access tokens

In version 2 the useAuth plugin does not automatically store the accessToken in the store, since the Feathers Client always holds a copy, which can be retrieved asynchronously. See the useAuth docs to see how to manually store the accessToken. Keep in mind that storing your accessToken in more places likely makes it less secure.

ts
// stores/auth.ts
import { acceptHMRUpdate, defineStore } from 'pinia'

export const useAuthStore = defineStore('auth', () => {
  const { userStore } = useUserStore()
  const { api } = useFeathers()

  const auth = useAuth({ api, userStore })

  return auth
})

if (import.meta.hot)
  import.meta.hot.accept(acceptHMRUpdate(useAuthStore, import.meta.hot))

Notice that we've called useAuth by providing the api and userStore. By providing the userStore, it will automatically add a returned user to the store after successful login. The above example also calls reAuthenticate, which checks for a valid, non-expired accessToken in the Feathers Client and automatically authenticates if one is found. It will fail silently to avoid the need to catch errors during app initialization.

The Auth store for Nuxt is different than the one in the Vite app. It does not call reAuthenticate inside the store. Instead, we will call it in the next step from within the auth plugin.

6.2 Auth Plugin

Now let's move on to create the feathers-auth plugin, which will use auth store. We'll prefix the filename with 2. in order to make sure it runs after the Feathers Client is created.

ts
// plugins/2.feathers-auth.ts
/**
 * Make sure reAuthenticate finishes before we begin rendering.
 */
export default defineNuxtPlugin(async (_nuxtApp) => {
  const auth = useAuthStore()
  await auth.reAuthenticate()
})

6.3 Route Middleware

With the auth store and plugin in place, we can now setup a route middleware to control the user's session. Create the following file to restrict non-authenticated users the routes in the publicRoutes array. Authenticated users will have access to all routes.

ts
// middleware/session.global.ts
export default defineNuxtRouteMiddleware(async (to, _from) => {
  const auth = useAuthStore()

  // Allow 404 page to show
  const router = useRouter()
  const allRoutes = router.getRoutes()
  if (!allRoutes.map(r => r.path).includes(to.path))
    return

  // if user is not logged in, redirect to '/' when not navigating to a public page.
  const publicRoutes = ['/', '/login']
  if (!auth.user?.value) {
    if (!publicRoutes.includes(to.path))
      return navigateTo('/')
  }
})

Instead of blindly redirecting to the login page, the middleware allows the 404 page to work by bringing in the list of allRoutes and checking the current route against the list.

What's Next?

Check out the full example app: feathers-pinia-nuxt3. Check out the login component to see an example of signup/login.

Many thanks go to the Vue, Vuex, Pinia, and FeathersJS communities for keeping software development FUN!