🧰 Minimal Nuxt 3 starter with Vuetify, Pinia and Google login

I believe you have already read my 👉 Nuxt 3 migration guide 👈.
And in this blog, I’ll be covering exactly what was mentioned there
an example demo project that includes Vue 3, Nuxt 3, Vuetify 3, Pinia, PWA, and Firebase authentication.
Find the source code here, making it an excellent starter for those new to Nuxt 3.

The motivation behind creating this starter was rooted in the complexity of the project I was migrating. It became challenging to pinpoint certain errors, making the debugging process cumbersome. Thus, having a minimal demo project like this proved to be invaluable, as it allows for swift code validation and configuration testing. With this straightforward starter, you can focus on honing your skills and building your Nuxt 3 applications with confidence.

🚛 Sure, let me walk you through how I built up this starter:

  1. Initializing a Nuxt 3 Project
  2. Adding TypeScript Configuration
  3. Adding ESLint and lint-staged
  4. Adding Internationalization (i18n)
  5. Adding Vuetify
  6. Utilizing SCSS Variables
  7. Adding Pinia
  8. Adding Firebase Google Login

Step 1: Initializing a Nuxt 3 Project

To kickstart our journey with Nuxt 3, the first step is to create a new project. Keep in mind that Nuxt 3 requires Node.js version 16.10.0 or newer, so ensure you have the appropriate version installed on your machine.

To create the project, open your terminal and run the following command, replace <project-name> with the desired name for your project. This command will initialize a new Nuxt 3 project in the specified folder:

1
npx create-nuxt-app <project-name>

Next, navigate to the project folder and then run:

1
2
cd <project-name>
code .

It’s time to install the necessary dependencies. With everything set up, the next command allowed to run the Nuxt 3 project locally and start developing:

1
2
yarn install
yarn dev

Step 2: Adding TypeScript Configuration

To further enhance our Nuxt 3 project and leverage the benefits of TypeScript, let’s integrate TypeScript configuration. TypeScript brings static type checking, improved code readability, and better developer tooling, making it a valuable addition to the project.
First, install TypeScript using Yarn:

1
yarn add --dev typescript

Next, create a tsconfig.json file in the root of the project. This file serves as the configuration file for TypeScript, enabling us to customize its behavior according to the project’s needs. Here’s an example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// tsconfig.json
{
"compilerOptions": {
"target": "ES2018",
"module": "ESNext",
"moduleResolution": "node",
"lib": [
"ESNext",
"ESNext.AsyncIterable",
"DOM"
],
"esModuleInterop": true,
"allowJs": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"noEmit": true,
"experimentalDecorators": true,
"baseUrl": ".",
"paths": {
"~/*": [
"./*"
]
},
"types": [
"@types/node",
],
},
"vueCompilerOptions": {
"allowJs": true,
"extensions": [
".vue"
],
"target": 2,
},
"exclude": [
"node_modules",
".nuxt",
".firebase"
],
"extends": "./.nuxt/tsconfig.json"
}

Step 3: Adding ESLint and lint-staged

To maintain a consistent and error-free codebase, I implemented ESLint and lint-staged in our Nuxt 3 project. ESLint provides static code analysis to identify and fix code errors, while lint-staged allows us to run ESLint on staged files, ensuring that only the committed code meets our coding standards.
To begin, install ESLint and its associated dependencies using Yarn:

1
2
yarn add --dev eslint eslint-plugin-nuxt lint-staged
yarn add --dev @nuxtjs/eslint-config-typescript

Next, create an .eslintrc.js file in the root of the project. This file contains the ESLint configuration, including any rules and plugins we want to enforce. I customized the configuration to suit our project’s needs, considering both Vue.js and TypeScript specifics.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// .eslintrc
{
"root": true,
"env": {
"browser": true,
"node": true
},
"extends": [
"@nuxtjs/eslint-config-typescript",
"plugin:nuxt/recommended",
"eslint:recommended"
],
"ignorePatterns": [
"/lib/**/*",
"node_modules",
".nuxt"
],
"plugins": [],
"rules": {
"import/named": 0,
"vue/multi-word-component-names": 0,
"space-before-function-paren": "off"
}
}

In the package.json file, add the ESLint scripts and configuring lint-staged:

  • lint:js: This script runs ESLint to check all JavaScript, TypeScript, and Vue files in the project, excluding the ones specified in the .gitignore file.
  • lint: This is an alias for yarn lint:js, which means you can now use yarn lint to execute the ESLint checks.
  • lintfix: This script also runs ESLint, but with the –fix flag, which automatically attempts to fix any fixable issues in the code.
  • lint-staged: This configuration specifies that lint-staged should run ESLint with the –fix flag only on TypeScript (.ts) and Vue (.vue) files that are staged for a commit.
1
2
3
4
5
6
7
8
9
10
11
// package.json
"scripts": {
"lint:js": "eslint --ext .js,.ts,.vue --ignore-path .gitignore .",
"lint": "yarn lint:js",
"lintfix": "yarn lint:js --fix"
},
"lint-staged": {
"*.{ts,vue}": [
"eslint --fix"
]
},

Step 4: Adding Internationalization (i18n)

To cater to a global audience and provide a localized experience, I introduced internationalization (i18n) to our Nuxt 3 project. i18n enables us to translate the content and messages in our application into multiple languages, enhancing accessibility and user engagement.
First, install @nuxtjs/i18n using Yarn:

1
yarn add --dev @nuxtjs/i18n

Then, add @nuxtjs/i18n to the modules section in your nuxt.config. You can use either of the following ways to specify the module options:

1
2
3
4
5
6
7
8
9
10
11
export default defineNuxtConfig({
modules: [
'@nuxtjs/i18n',
],
i18n: {
locales: ['en', 'fr', 'ar'],
defaultLocale: 'en',
strategy: 'no_prefix',
vueI18n: './i18n.config.ts'
}
})

Next, create an i18n.config.ts file to set up the i18n configuration. In this file, I defined the supported languages, default language, and any other relevant settings for translation.

1
2
3
4
5
6
7
8
9
10
11
12
export default {
legacy: false,
locale: 'en',
allowComposition: true,
globalInjection: true,
fallbackLocale: 'en',
messages: {
en: { Dashboards: 'Dashboards' },
fr: { Dashboards: 'Tableaux de bord' },
ar: { Dashboards: 'لوحات القيادة' }
}
}

To allow users to switch between different languages, I added a language switcher to the user interface. This component lets users select their preferred language and updates the content accordingly.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<form>
<select v-model="locale">
<option value="en">en</option>
<option value="ar">ar</option>
<option value="fr">fr</option>
</select>
<p>{{ $t('Dashboards') }}</p>
</form>
</template>
<script setup>
import { useI18n } from 'vue-i18n'
const { locale } = useI18n()
</script>

Step 5: Adding Vuetify

To enrich the user interface and provide a cohesive design language, I integrated Vuetify into our Nuxt 3 project. Vuetify offers a comprehensive set of pre-designed components and styles, enabling us to create visually stunning and responsive user interfaces with minimal effort.
First, install vuetify using Yarn:

1
yarn add --dev vuetify

Next, add the necessary Vuetify configuration to the nuxt.config.js file.

1
build: { transpile: ['vuetify'] },

Then, create a plugin named vuetify.ts under plugins folder, ensures that Vuetify is properly loaded and available across the entire application.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { defineNuxtPlugin } from 'nuxt/app'

import 'vuetify/styles'
import { createVuetify } from 'vuetify'
import * as components from 'vuetify/components'
import * as directives from 'vuetify/directives'

export default defineNuxtPlugin((nuxtApp) => {
const vuetify = createVuetify({
ssr: true,
components,
directives
})

nuxtApp.vueApp.use(vuetify)
})

Vuetify comes with two themes pre-installed, light and dark. Now let’s add Vuetify theme toggle to validate if it can work.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<div>
<v-btn @click="toggleTheme">
toggle theme
</v-btn>
</div>
</template>

<script setup>
import { useTheme } from 'vuetify'
const theme = useTheme()
const toggleTheme = () => {
theme.global.name.value = theme.global.current.value.dark ? 'light' : 'dark'
}
</script>

Step 6: Utilizing SCSS Variables

To leverage the power of SCSS variables in our Nuxt 3 project and enhance code reusability and maintainability, I introduced SCSS variables. SCSS variables allow us to store commonly used values, such as colors or font sizes, in one place and use them across our stylesheets.
In the Vuetify configuration within the plugins/vuetify.ts file, I used SCSS variables to define custom theme colors.

1
2
3
4
5
6
7
// plugins/vuetify.ts
light: {
dark: false,
variables: {
'my-color-value': '#81df3a'
}
}

In the styles.scss file, I defined the SCSS variables $my-color1 and $my-color2, representing colors used in the application. By using SCSS variables, we can easily change these colors in one place and see the changes propagate throughout the project.

1
2
3
// styles.scss
$my-color1: rgb(var(--v-my-color-value));
$my-color2: #6928d0;
1
2
3
4
5
6
7
8
9
10
11
<template>
<!-- Component template goes here -->
</template>
<style lang="scss" scoped>
@import 'styles/styles.scss';
.form {
width: 100px;
background-color: $my-color1;
color: $my-color2;
}
</style>

Step 7: Adding Pinia

To enhance state management and improve the overall architecture of our Nuxt 3 project, I integrated Pinia. Pinia is a modern, Vue.js-based state management solution that offers simplicity, reactivity, and excellent TypeScript support.
First, install TypeScript using Yarn:

1
yarn add pinia @pinia/nuxt

Then add it to modules in nuxt.config.js file:

1
2
3
4
5
6
// nuxt.config.js
export default defineNuxtConfig({
modules: [
'@pinia/nuxt',
],
})

By default @pinia/nuxt exposes one single auto import: usePinia(), you can add auto imports for other Pinia related utilities like this:

1
2
3
4
5
6
// nuxt.config.js
export default defineNuxtConfig({
pinia: {
autoImports: ['defineStore', 'storeToRefs']
},
})

Here’s a simple example defining a store using pinia in Option API.

1
2
3
4
5
6
7
8
9
10
11
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0, name: 'Eduardo' }),
getters: {
doubleCount: state => state.count * 2
},
actions: {
increment() {
this.count++
}
}
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<p>
{{ name }} {{ doubleCount }}
</p>
<v-btn @click="increment">
count++
</v-btn>
</template>

<script setup>
const store = useCounterStore()
const { name, doubleCount } = storeToRefs(store)
const { increment } = store
</script>

Step 8: Adding Firebase Google Login

To enable users to log in to our Nuxt 3 application using their Google accounts, we can integrate Firebase Authentication. Firebase Authentication offers a secure and straightforward way to authenticate users and manage their login sessions.
First, install firebase dependencies:

1
yarn add firebase firebase-admin

Create a new file named firebase.client.ts inside the plugins folder. In this file, initialize Firebase for the client-side of our Nuxt 3 application:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// plugins/firebase.client.ts
import { initializeApp, getApps } from 'firebase/app'
import { getAuth } from 'firebase/auth'

export default defineNuxtPlugin(({ provide }) => {
const runtimeConfig = useRuntimeConfig()
const config = process.env.NODE_ENV === 'development' ? firebaseConfig : runtimeConfig.firebase
const firebaseApp = getApps()[0] ?? initializeApp(config)
const firebaseAuth = getAuth(firebaseApp)

// set user on client when available
firebaseAuth.onAuthStateChanged(signInCallback)

provide('firebase', firebaseApp)
provide('auth', firebaseAuth)

if ('serviceWorker' in window.navigator && process.env.NODE_ENV !== 'development') {
window.navigator.serviceWorker.register('/sw.js')
}
})

const signInCallback = (token) => {
const { email, accessToken } = token || {
email: undefined,
token: undefined
}
if (!email || !accessToken) { return }

const { user } = useAuth()
user.value = { email }

// set cookie for server side
const cookie = useCookie('authToken')
if (!cookie.value) { cookie.value = accessToken }
}

For server-side rendering (SSR), you may need a separate Firebase configuration file named firebase.server.ts inside the plugins folder. However, since Firebase Authentication works on the client-side, you may not need this file for our current implementation.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// plugins/firebase.server.ts
import admin, { ServiceAccount } from 'firebase-admin'
import 'firebase/compat/auth'
import serviceAccount from '../firebase.json'

export default defineNuxtPlugin(({ ssrContext }) => {
admin.apps?.length === 0 &&
admin.initializeApp({
credential: admin.credential.cert(serviceAccount as ServiceAccount)
})

// get auth token from headers
const tokenHeader = ssrContext.event.req.headers.authorization?.substring('Bearer '.length)
const tokenCookie = useCookie('authToken')

if (!tokenHeader && !tokenCookie.value) { return }

const { user } = useAuth()
admin
.auth()
.verifyIdToken(tokenHeader || tokenCookie.value)
// get properties from decoded id token
.then(({ email }) => (user.value = { email }))
})

Create a new file named useAuth.ts inside the composables folder. This composable will handle Firebase Authentication and provide methods for signing in with Google, signing out, and accessing the user’s information:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// composables/useAuth.ts
import { GoogleAuthProvider, signInWithPopup } from 'firebase/auth'

export const useAuth = () => {
const { $auth } = useNuxtApp()
const user = useState('user', undefined)

const signInWithGoogle = () => {
const provider = new GoogleAuthProvider()
signInWithPopup($auth, provider).catch(e => console.error(e))
}

const signOut = () => {
$auth.signOut()
user.value = undefined
useCookie('authToken').value = undefined
}

return { user, signInWithGoogle, signOut }
}

Now, create a new Vue component named Login.vue in the appropriate folder. This component will display a “Sign In with Google” button when the user is not logged in and a “Welcome back” message with a “Sign Out” button when the user is logged in:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<div>
<div v-if="user">
Welcome back, {{ user?.email }}
<v-btn @click="signOut">
Sign Out
</v-btn>
</div>
<v-btn v-else @click="signInWithGoogle">
Sign In With Google
</v-btn>
</div>
</template>

<script setup>
const { signInWithGoogle, signOut, user } = useAuth()
</script>

With these components and configurations in place, our Nuxt 3 project is now equipped with Firebase Google Login. Users can easily sign in using their Google accounts, and once authenticated, the application can display personalized content and features based on the user’s login status.

Feel free to customize and expand upon this blog post to include additional features and considerations specific to your project. Happy writing and best of luck with your Nuxt 3 application!
Find the source code here, making it an excellent starter for those new to Nuxt 3.


References:

🔍 Check out the demo project in github.
📮 If find any errors, please feel free to discuss and correct them: bsu5@ncsu.edu.