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:
- Create a Nuxt app
- Use the starter project and read the below as reference. OR
- Start a new Nuxt app and follow the below as instructions.
- Install Feathers-Pinia,
- 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:
- @pinia/nuxt module
- nuxt-feathers-pinia module
- Nuxt
imports
config - Nuxt Takeover Mode
- Vue Reactivity Transform
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:
- DRY code: changing a shared value in one place updates it for all services.
- 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 instores
. - 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.