Skip to main content

Quickstart: Vue

Introduction

This quickstart guide provides the steps you need to build a simple Vue app powered by Nhost for the backend. It includes:

By the end of this guide, you'll have a full-stack app that allows users to log in to access a protected dashboard and update their profile information.

tip

You can see the result of this quickstart in our main repository.

You can also preview it in the browser: StackBlitz

Prerequisites

Before getting started, let's make sure that your development environment is ready.

You'll need Node.js version 14 or later: install it from here.

Project setup

Create a new Nhost app

First things first, we need to create a new Nhost project.

So, log in to your Nhost dashboard and click the Create Your First Project button.

Nhost Dashboard

Next, give your new Nhost project a name, select a geographic region for your Nhost services and click Create Project.

New Nhost project

After a few seconds, you should get a PostgreSQL database, a GraphQL API with Hasura, file storage, and authentication set up.

info

You can also connect your Nhost project to a Git repository at GitHub. When you do this, any updates you push to your code will automatically be deployed. Learn more

Initialize the app

Create a Vue app

We will use a simple adaptation of Vitesse Lite, a ready-to-deploy Vite template by Anthony Fu. We can scaffold it with degit.

Open your terminal, and run the following command:

npx degit nhost/vue-quickstart my-nhost-app

Sidebar

A completed quickstart example can be be found in the examples directory.

Run the following command in your terminal to create a local copy the example project:

npx degit nhost/nhost/examples/vue-quickstart my-nhost-app

You can now go into your project directory, install dependencies, and start the development server:

cd my-nhost-app
npm install
npm dev

If everything is working fine, your Vue development server should be running on port 3000. Open http://localhost:3000 from your browser to check this out.

Configure Nhost with Vue

To work with Nhost from within our Vue app, we'll use the Vue SDK provided by Nhost. It's a wrapper around the Nhost JavaScript SDK which gives us a way to interact with our Nhost backend using Vue composables.

You can install the Nhost Vue SDK with:

npm install @nhost/vue graphql

Next, open your src/main.ts file as we'll now configure Nhost inside our app.

The Nhost Vue SDK comes with a NhostClient that can be loaded into the Vue application as a plugin. It makes the authentication state and all the provided Vue composables available in our application.

Ensure the hightlighted code below is present to instantiate a new Nhost client and link it to your Nhost backend:

src/main.ts
import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import routes from 'virtual:generated-pages'
// highlight-start
import { NhostClient } from '@nhost/vue'
// highlight-end
import App from './App.vue'

import '@unocss/reset/tailwind.css'
import './styles/main.css'
import 'uno.css'

// highlight-start
const nhost = new NhostClient({
subdomain: import.meta.env.VITE_NHOST_SUBDOMAIN
region: import.meta.env.VITE_NHOST_REGION
})
// highlight-end

const app = createApp(App)
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes
})
app
.use(router)
// highlight-start
.use(nhost)
// highlight-end
.mount('#app')

Finally, rename the existing .env.example file as .env or .env.development and store the environment variables for subdomain and region in the file like this:

.env.development
VITE_NHOST_SUBDOMAIN=[subdomain]
VITE_NHOST_REGION=[region]

You find your Nhost project's subdomain and region in the project overview:

Project Overview

Nhost CLI

Do you use the Nhost CLI? Learn how to set subdomain and region in the CLI documentation.

Build the app

Add authentication

1. Sign-up

The next step is to allow our users to authenticate into our application. Let's start with implementing the sign-up process.

For that, we'll use the useSignUpEmailPassword composable provided by the Nhost Vue SDK within a /sign-up page.

Let's create a new page in your project using the following code:

src/pages/sign-up.vue
<script setup lang="ts">
import { ref } from 'vue'
import { useSignUpEmailPassword } from '@nhost/vue'
import { useRouter } from 'vue-router'

const { signUpEmailPassword, needsEmailVerification } = useSignUpEmailPassword()
const router = useRouter()
const firstName = ref('')
const lastName = ref('')
const email = ref('')
const password = ref('')
const handleSubmit = async (event: Event) => {
event.preventDefault()
const { isSuccess } = await signUpEmailPassword(email, password, {
metadata: { firstName, lastName }
})
if (isSuccess)
router.push('/')
}
</script>

<template>
<p v-if="needsEmailVerification">
Please check your mailbox and follow the verification link to verify your email.
</p>

<form v-else @submit="handleSubmit">
<input v-model="firstName" placeholder="First name" class="input" />
<br />
<input v-model="lastName" placeholder="Last name" class="input" />
<br />
<input v-model="email" type="email" placeholder="Email" class="input" />
<br />
<input v-model="password" type="password" placeholder="Password" class="input" />
<br />

<button class="btn-submit" type="submit">
Sign up
</button>
</form>
</template>

2. Sign-in

Now that new users can sign up for our application, let's see how to allow existing users to sign in with email and password.

For that, we will use the Nhost composable named useSignInEmailPassword inside a new sign-in page the same way we did with our sign-up page.

Let's create a src/pages/sign-in.vue component:

src/pages/sign-in.vue
<script setup lang="ts">
import { ref } from 'vue'
import { useSignInEmailPassword } from '@nhost/vue'
import { useRouter } from 'vue-router'

const { signInEmailPassword, needsEmailVerification } = useSignInEmailPassword()
const router = useRouter()
const email = ref('')
const password = ref('')
const handleSubmit = async (event: Event) => {
event.preventDefault()
const { isSuccess } = await signInEmailPassword(email, password)
if (isSuccess)
router.push('/')
}
</script>

<template>
<p v-if="needsEmailVerification">
Your email is not yet verified. Please check your mailbox and
follow the verification link to finish registration.
</p>

<form v-else @submit="handleSubmit">
<input v-model="email" type="email" placeholder="Email" class="input" />
<br />
<input v-model="password" type="password" placeholder="Password" class="input" />
<br />

<button class="btn-submit" type="submit">
Sign in
</button>
</form>
</template>

3. Home page

Let's also add links to sign up and sign in in our index page.

src/pages/index.vue
<template>
<div>
<div i-carbon-home text-4xl inline-block />
<p>Nhost with Vue</p>
<p>
<em text-sm op75>Quickstart</em>
</p>
<div py-4 />
<!-- highlight-start -->
<router-link class="btn" to="/sign-up">
Sign Up
</router-link>
<br />
<router-link class="btn" to="/sign-in">
Sign In
</router-link>
<!-- highlight-end -->
</div>
</template>

4. Sign-out

Finally, to allow the users to sign out from the app, we can use the Nhost useSignOut composable. We'll also use useAuthenticationStatus to show the button only when the user is authenticated:

src/components/Footer.vue
<script setup lang="ts">
import { useRouter } from 'vue-router'
import { useAuthenticated, useSignOut } from '@nhost/vue'
import { isDark, toggleDark } from '~/composables'

const isAuthenticated = useAuthenticated()
const { signOut } = useSignOut()
const router = useRouter()
const handleSignOut = () => {
signOut()
router.push('/')
}
</script>

<template>
<nav text-xl mt-6 inline-flex gap-2>
<button class="icon-btn !outline-none" @click="toggleDark()">
<div v-if="isDark" i-carbon-moon />
<div v-else i-carbon-sun />
</button>

<button v-if="isAuthenticated" class="icon-btn !outline-none" @click="handleSignOut">
<div i-carbon-logout />
</button>
</nav>
</template>

Protect routes

Now that we have implemented authentication, we can easily decide who can access certain parts of our application.

Let's create a profile page that will be only accessible to authenticated users. If an unauthenticated user attempts to load it, it will redirect them to the /sign-up page:

src/pages/profile.vue
<template>
<div>
<div i-carbon-home text-4xl inline-block />
<p>Profile page</p>
</div>
</template>

Then, we can use a beforeEach navigation guard in our main.ts file:

src/main.ts
import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import routes from 'virtual:generated-pages'
import { NhostClient } from '@nhost/vue'
import App from './App.vue'

import '@unocss/reset/tailwind.css'
import './styles/main.css'
import 'uno.css'

const nhost = new NhostClient({
subdomain: import.meta.env.VITE_NHOST_SUBDOMAIN,
region: import.meta.env.VITE_NHOST_REGION
})

const app = createApp(App)
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes
})

// highlight-start
router.beforeEach(async (to) => {
if (to.path === '/profile' && !(await nhost.auth.isAuthenticatedAsync())) {
return '/sign-in'
}
return true
})
// highlight-end

app
.use(router)
.use(nhost)
.mount('#app')

Add a link to the profile page in the index page /:

src/pages/index.vue
<template>
<div>
<div i-carbon-home text-4xl inline-block />
<p>Nhost with Vue</p>
<p>
<em text-sm op75>Quickstart</em>
</p>
<div py-4 />
<!-- highlight-start -->
<router-link class="btn" to="/profile">
Profile
</router-link>
<br />
<!-- highlight-end -->
<router-link class="btn" to="/sign-up">
Sign Up
</router-link>
<br />
<router-link class="btn" to="/sign-in">
Sign In
</router-link>
</div>
</template>

Retrieve user data

Finally, let's display the information of the authenticated user throughout their dashboard to make the app more personalized.

Getting the current authenticated user data is quite easy. Indeed, we can use the useUserData composable provided by Nhost to do it.

When the user is authenticated, it returns the information fetched from the users table, such as the display name, the email, or the user's roles. This composable returns null until the user is effectively authenticated.

Let's update the profile page to use it:

src/pages/profile.vue
<!-- highlight-start -->
<script setup lang="ts">
import { useUserData } from '@nhost/vue'
const user = useUserData()
</script>
<!-- highlight-end -->
<template>
<div>
<div i-carbon-home text-4xl inline-block />
<p>Profile page</p>
<!-- highlight-start -->
<p>
<em text-sm op75>Quickstart</em>
</p>
<div v-if="user" py-4>
<p my-4>
Hello, {{ user?.displayName }}. Your email is {{ user?.email }}.
</p>
</div>
<!-- highlight-end -->
</div>
</template>

Update user data

Nhost provides a GraphQL API through Hasura so that we can query and mutate our data instantly.

In this tutorial, we'll use Vue Apollo v4 for interacting with this GraphQL API. Nhost comes with a custom Apollo client that syncs the Apollo client with the authentication status of your users.

So, we start by installing the following dependencies:

npm install @nhost/apollo @apollo/client graphql graphql-tag @vue/apollo-composable

Then, create the Apollo client in your src/main.ts file, and provide it to your Vue app:

src/main.ts
import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import routes from 'virtual:generated-pages'
import { NhostClient } from '@nhost/vue'
// highlight-start
import { createApolloClient } from '@nhost/apollo'
import { DefaultApolloClient } from '@vue/apollo-composable'
// highlight-end
import App from './App.vue'

import '@unocss/reset/tailwind.css'
import './styles/main.css'
import 'uno.css'

const nhost = new NhostClient({
subdomain: import.meta.env.VITE_NHOST_SUBDOMAIN,
region: import.meta.env.VITE_NHOST_REGION
})

const app = createApp(App)
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes
})

router.beforeEach(async (to) => {
if (to.path === '/profile' && !(await nhost.auth.isAuthenticatedAsync())) {
return '/sign-in'
}
return true
})

// highlight-start
const apolloClient = createApolloClient({ nhost })
// highlight-end

app
.use(router)
.use(nhost)
// highlight-start
.provide(DefaultApolloClient, apolloClient)
// highlight-end
.mount('#app')

From there, we can construct our GraphQL query and use the Apollo useMutation composable to execute that query when the user submits the form from the profile page:

src/pages/profile.vue
<script setup lang="ts">
// highlight-start
import { gql } from '@apollo/client/core'
import { useNhostClient, useUserData } from '@nhost/vue'
import { useMutation } from '@vue/apollo-composable'
import { ref } from 'vue'
// highlight-end
const user = useUserData()
// highlight-start
const { nhost } = useNhostClient()

const UPDATE_USER_MUTATION = gql`
mutation ($id: uuid!, $displayName: String!, $metadata: jsonb) {
updateUser(
pk_columns: { id: $id }
_set: { displayName: $displayName, metadata: $metadata }
) {
id
displayName
metadata
}
}
`
const firstName = ref('')
const lastName = ref('')
const { mutate, loading, error } = useMutation(UPDATE_USER_MUTATION)

const updateUserProfile = async (event: Event) => {
event.preventDefault()
if (user.value) {
await mutate({
id: user.value.id,
displayName: `${firstName.value} ${lastName.value}`.trim(),
metadata: {
firstName: firstName.value,
lastName: lastName.value
}
})
await nhost.auth.refreshSession()
}
}
// highlight-end
</script>

<template>
<div>
<div i-carbon-home text-4xl inline-block />
<p>Profile page</p>
<p>
<em text-sm op75>Quickstart</em>
</p>
<div v-if="user" py-4>
<p my-4>
Hello, {{ user?.displayName }}. Your email is {{ user?.email }}.
</p>
<!-- highlight-start -->
<form @submit="updateUserProfile">
<input v-model="firstName" placeholder="First name" class="input" />
<br />
<input v-model="lastName" placeholder="Last name" class="input" />
<br />
<button className="m-3 text-sm btn" :disabled="loading">
Save
</button>
<div v-if="error">
{{ error.message }}
</div>
</form>
<!-- highlight-end -->
</div>
</div>
</template>
tip

You probably have noticed that we are calling nhost.auth.refreshSession() after we updated the user using the GraphQL mutation. The Nhost client only extracts user information from the access token (JWT), that is kept in memory and refreshed every 15 minutes. As user information has been updated, we force an access token refresh so it is kept up to date.

Finally, since Hasura has an allow nothing by default policy, and we haven't set any permissions yet, our GraphQL mutations would fail.

So, open the Hasura console from the Data tab of your project from your Nhost dashboard. Then, go to the permissions tab of the users table, type in user in the role cell, and click the edit icon on the select operation:

Hasura users permissions

To restrict the user to read his own data only, specify a condition with the user's ID and the X-Hasura-User-ID session variable, which is passed with each requests.

Hasura users permissions

Next, select the columns you'd like the users to have access to, and click Save Permissions.

Hasura users permissions

Important

Repeat the same steps on the update operation for the user role to allow users to update their displayName and metadata only.

Adding Real-time synchronizing

To add real-time caching, synchronizing, and updating server state in your Vue app, let's refactor the user data fetching using the Apollo client and our GraphQL API instead.

First, update the profile page, and replace the useUserData composable with the useUserId composable to retrieve the current user's ID.

src/pages/profile.vue
// replace `import { useNhostClient, useUserData } from '@nhost/vue'`
import { useNhostClient, useUserId } from '@nhost/vue'

// replace `const user = useUserData()`
const id = useUserId()

Then import the computed() function from Vue.

src/pages/profile.vue
// replace `import { ref } from 'vue'`
import { computed, ref } from 'vue'

Then and add the highlighted code below to add a GraphQL subscription that retrieves the current user data component:

src/pages/profile.vue
// snip...
const { nhost } = useNhostClient()

// highlight-start
const GET_USER_SUBSCRIPTION = gql`
subscription GetUser($id: uuid!) {
user(id: $id) {
id
email
displayName
metadata
avatarUrl
}
}
`
// highlight-end

const id = useUserId()

// highlight-start
const { result } = useSubscription(
GET_USER_SUBSCRIPTION,
computed(() => ({ id: id.value }))
)
const user = computed(() => result.value?.user)
// highlight-end

// snip...

We can now run our GraphQL subscription using the useSubscription composable and retrieve the current user's ID.

The completed profile.vue page should look like this:

src/pages/profile.vue
<script setup lang="ts">
import { gql } from '@apollo/client/core'
import { useNhostClient, useUserId } from '@nhost/vue'
import { useMutation, useSubscription } from '@vue/apollo-composable'
import { computed, ref } from 'vue'

const { nhost } = useNhostClient()

const GET_USER_SUBSCRIPTION = gql`
subscription GetUser($id: uuid!) {
user(id: $id) {
id
email
displayName
metadata
avatarUrl
}
}
`
const id = useUserId()

const { result } = useSubscription(
GET_USER_SUBSCRIPTION,
computed(() => ({ id: id.value }))
)
const user = computed(() => result.value?.user)

const UPDATE_USER_MUTATION = gql`
mutation ($id: uuid!, $displayName: String!, $metadata: jsonb) {
updateUser(
pk_columns: { id: $id }
_set: { displayName: $displayName, metadata: $metadata }
) {
id
displayName
metadata
}
}
`
const firstName = ref('')
const lastName = ref('')
const { mutate, loading, error } = useMutation(UPDATE_USER_MUTATION)

const updateUserProfile = async (event: Event) => {
event.preventDefault()
if (user.value) {
await mutate({
id: user.value.id,
displayName: `${firstName.value} ${lastName.value}`.trim(),
metadata: {
firstName: firstName.value,
lastName: lastName.value
}
})
await nhost.auth.refreshSession()
}
}
</script>

<template>
<div>
<div i-carbon-home text-4xl inline-block />
<p>Profile page</p>
<p>
<em text-sm op75>Quickstart</em>
</p>
<div v-if="user" py-4>
<p my-4>
Hello, {{ user.displayName }}. Your email is {{ user.email }}.
</p>
<form @submit="updateUserProfile">
<input v-model="firstName" placeholder="First name" class="input" />
<br />
<input v-model="lastName" placeholder="Last name" class="input" />
<br />
<button className="m-3 text-sm btn" :disabled="loading">
Save
</button>
<div v-if="error">
{{ error.message }}
</div>
</form>
</div>
</div>
</template>

You now have a fully functional Vue application. Congratulations!

Next Steps

  • Did you enjoy Nhost? Give us a star ⭐ on Github. Thank you!
  • Check out our more in-depth examples.
  • Build your next app with Nhost!