Starting a Vite Project
Learn how to setup Feathers-Pinia 3 with Vite.
Prerequisites
Follow these steps to get started with a new single-page Vite app:
- Create a Vue app with Vite.
- Install packages,
- Follow the instructions, below
1. Feathers Client
The first step is to setup a Feathers Client.
The new FeathersJS v5 Dove CLI now creates a fully-typed Feathers Client for you. The next examples use the new CLI-generated client that comes with Dove apps.
Note about Feathers Types
Replace my-feathers-api
in the below example with the package installed from your Feathers v5 Dove API. You can manually create a client. See the install page for more details.
// src/feathers.ts
import { createClient } from 'feathers-pinia-api'
import socketio from '@feathersjs/socketio-client'
import io from 'socket.io-client'
const host = import.meta.env.VITE_MY_API_URL as string || 'http://localhost:3030'
const socket = io(host, { transports: ['websocket'] })
const feathersClient = createClient(socketio(socket), { storage: window.localStorage })
// src/feathers.ts
import { createClient } from 'feathers-pinia-api'
import rest from '@feathersjs/rest-client'
const host = import.meta.env.VITE_MY_API_URL as string || 'http://localhost:3030'
const fetch = window.fetch.bind(window)
const feathersClient = createClient(rest(host).fetch(fetch), { storage: window.localStorage })
// src/feathers.ts
import { type Service, feathers } from '@feathersjs/feathers'
import authenticationClient from '@feathersjs/authentication-client'
import socketio from '@feathersjs/socketio-client'
import io from 'socket.io-client'
// Define your custom types (usually imported from another file)
export interface Book {
_id: string
title: string
}
// Create a ServiceTypes generic
export interface ServiceTypes {
'book': Service<Book>
}
const host = import.meta.env.VITE_MY_API_URL as string || 'http://localhost:3030'
const socket = io(host, { transports: ['websocket'] })
export const feathersClient = feathers<ServiceTypes>()
.configure(socketio(socket))
.configure(authenticationClient({ storage: window.localStorage }))
// src/feathers.ts
import { type Service, feathers } from '@feathersjs/feathers'
import authenticationClient from '@feathersjs/authentication-client'
import rest from '@feathersjs/rest-client'
// Define your custom types (usually imported from another file)
export interface Book {
_id: string
title: string
}
// Create a ServiceTypes generic
export interface ServiceTypes {
'book': Service<Book>
}
const host = import.meta.env.VITE_MY_API_URL as string || 'http://localhost:3030'
const fetch = window.fetch.bind(window)
export const feathersClient = feathers<ServiceTypes>()
.configure(rest(host).fetch(fetch))
.configure(authenticationClient({ storage: window.localStorage }))
You can see an SSG-compatible localStorage example on the Common Patterns page.
Now add this code to the bottom of the same file:
// src/feathers.ts
export const api = createPiniaClient(feathersClient, {
pinia,
idField: '_id',
// optional
ssr: false,
whitelist: [],
paramsForServer: [],
skipGetIfExists: true,
customSiftOperators: {},
setupInstance(data) {
return data
},
customizeStore(defaultStore) {
return {}
},
services: {},
})
See a full explanation of options on the createPiniaClient page.
The above code wraps the feathersClient
into a Feathers-Pinia turbocharged client. To create or reference a store, you just use the service like you would with a plain Feathers Client:
// Creates a 'users' service store and fetches data
api.service('users').get(1)
Next we need to setup Pinia.
2. Pinia
Create the file src/modules/pinia.ts
and paste in the following code, which creates a pinia
instance:
// src/modules/pinia.ts
import { createPinia } from 'pinia'
export const pinia = createPinia()
Now in src/main.ts
, import the pinia
instance we just created and pass it to app.use(pinia)
. Here's an example from this example app feathers-pinia-vite:
// src/main.ts
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import { router } from './router'
import { pinia } from './modules/pinia'
import { createHead } from '@vueuse/head'
const head = createHead()
createApp(App).use(pinia).use(router).use(head).mount('#app')
3. useFeathers
and Auto-Imports
To make Feathers Client easily accessible, we'll create a composable called useFeathers
.
// src/composables/feathers.ts
import { api } from '../feathers'
// Provides access to Feathers Client(s)
export function useFeathers() {
return { api }
}
Next, setup Auto-Imports for Vite. Auto-imports allow us to use functions without importing them. We can retrieve the api
in one line of code, now:
const { api } = useFeathers()
4. 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.
4.1 Auth Store
Feathers-Pinia 3.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 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.
// stores/auth.ts
import { acceptHMRUpdate, defineStore } from 'pinia'
export const useAuthStore = defineStore('auth', () => {
const { api } = useFeathers()
const auth = useAuth({ api, servicePath: 'users' })
auth.reAuthenticate()
return auth
})
if (import.meta.hot)
import.meta.hot.accept(acceptHMRUpdate(useAuthStore, import.meta.hot))
Notice that we've called useAuth
with the api
and userStore
. Providing the userStore
automatically adds the 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.
4.2 App.vue
Updates
With the auth store in place, we can now use it in our App.vue file to only show the UI once auth initialization has completed. The auth store includes an isInitDone
attribute to handle this scenario. It will become true
after auth either succeeds or fails. Assuming you've created a Loading
component (not shown), you could show the loading screen by using v-if="authStore.isInitDone
, as shown here:
// src/App.vue
<script setup lang="ts">
const authStore = useAuthStore()
</script>
<template>
<RouterView v-if="authStore.isInitDone" />
<Loading v-else />
</template>
Now a loading screen will show until auth is ready.
4.3 Route Middleware
The final step is to protect our routes with Route Middleware, also known as navigation guards.
Let's create a route middleware to control the user's session. The following file limits non-authenticated users to see the routes listed in the publicRoutes
array. Authenticated users will have access to all routes. The example assumes you've installed vite-plugin-pages and vite-plugin-vue-layouts, which enable layouts and file-based routing rules similar to how Nuxt works.
The route middleware starts with router.beforeEach
.
// src/router.ts
import { createRouter, createWebHistory } from 'vue-router'
import { setupLayouts } from 'virtual:generated-layouts'
import generatedRoutes from '~pages'
const routes = setupLayouts(generatedRoutes)
export const router = createRouter({
history: createWebHistory(),
routes,
})
router.beforeEach(async (to, from) => {
const authStore = useAuthStore()
const publicRoutes = ['/', '/login']
const is404 = to.matched[0].name === 'NotFound'
if (publicRoutes.includes(to.path) || is404)
return true
// for non-public routes, check auth and apply login redirect
await authStore.getPromise()
if (!authStore.user) {
authStore.loginRedirect = to
return { path: '/login' }
}
return true
})
Instead of blindly redirecting to the login page, the above code allows the 404 page to work. It also uses a "login redirect", which means it checks if a non-logged in user tries to access a page that requires authentication. It stores the loginRedireect
so that after successful login the login page can redirect the user to the page they were trying access in the first place.
What's Next?
Learn how to query data with a Feathers-Pinia service.