Building A Holiday Project with Vue 3.

Reusable Components | Routes & Protected Routes | Vuex | Axios | Nodejs & MongoDB.

Building an amazing and valuable client-side application with a UI framework like React, Vue.Js is a skill I'm into and becoming better at with the Altschool program.

There are various published articles on the advantages of using Vue.js. This framework offers several benefits, which includes its simplicity, ease of learning for interested developers, and reusability: that makes development faster and time efficient. Its virtual DOM rendering and optimised rendering algorithm provides excellent performance, according to pixelcrayons.

So, this is a good tool to build with.

About My Project

During my second semester exam, I solved the task on authentication and authorization. I built a movie review app, that allows users get a review of top Netflix movies. Users are expected to have an existing account to use the application. While I wrote the code in react, I was not satisfied with whatever I submitted.

So, this is an updated version written in Vue.js. The user is also expected to create an account to use the application. I simulated a server database with node.js/express, to have sensitive user data saved away from the client.

So, here is a guide on how I went about it. Meanwhile, you can take a look at my project GitHub repo here, for the completed project source code.

Prerequisite

As a javascript developer, I had these tool setups on local system.

  • A preferred browser, e.g Chrome,

  • An IDE, I used Visual Studio Code (can install here),

  • Git (install here),

  • Nodejs (install here).

Vue.Js Setup

I created the project by using a build setup based on Vite, allowing the use of Vue's Single-File-Components (SFC). Incase you choose to follow along, I'd suggest you do same, because it does the heavy lifting setup for you by scaffolding your application.

Here is the command line I used to scaffold my application:

npm create vue@latest

Once the command executes, you'd be prompted to choose option support features for your projects. If you aren't sure, choose 'No'. Afterwards, the project is created.

I followed the below steps to install dependencies, and run my development server.

cd <your-project-name>
npm install
npm run dev

Web Page Setup

I created the following pages that are mounted based on a route request.

  • The hero page/landing page: This page is mounted on the root route to direct user create account.

  • Register Component: This is the register form collecting the user credentials for authentication and authorization.

  • Login Component: This is the login form collecting the user credentials for authentication and authorization.

  • Dashboard Component: This is protected and only accessible after the user has been authenticated and authorized.

Routing

I implemented routing to enable user navigate through the application. With importing 'vue-router' into the route component, I used 'createRouter' and 'createWebHistory' to setup routing in vue, as below:

//router.js
import { createRouter, createWebHistory } from 'vue-router';
import AppLayout from '@/layouts/AppLayout.vue';
import AuthLayout from '@/layouts/AuthLayout.vue';
import HeroPage from '@/views/HeroPage.vue';
const Dashboard = ()=> import('@/views/Dashboard.vue')
const Register = ()=> import('@/views/Register.vue')
const Login = ()=> import('@/views/Login.vue')
const Resetpassword = ()=> import('@/views/Resetpassword.vue')

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  scrollBehavior(to, from, savedPosition){
    if(to.hash){
      return {
        el: to.hash,
        behavior: 'smooth'
      }
    }
    if(savedPosition){
        return {
          savedPosition,
          behavior: 'smooth'
        }
    }
    return { top: 0, behavior: 'smooth'}
  },
  routes: [
    {
      path: '/',
      name: 'app-layout',
      component: AppLayout,
      children: [
        {
          path: '',
          name: 'hero-page',
          component: HeroPage,
        },
        {
          path: 'dashboard',
          name: 'dashboard',
          component: Dashboard,
          meta: { requiresAuth: true },
        },
      ],
    },
    {
      path: '/auth/',
      name: 'auth-layout',
      component: AuthLayout,
      redirect: { name: 'login'},
      children: [
        {
          path: '/login',
          name: 'login',
          component: Login,
        },
        {
          path: '/register',
          name: 'register',
            component: Register,
        },
        {
          path: '/reset',
          name: 'reset',
          component: Resetpassword,
        },
      ],
    },
  ],
});

router.beforeEach((to, from, next) => {
  const loggedIn = localStorage.getItem('user');

  if (to.matched.some((record) => record.meta.requiresAuth) && !loggedIn) {
    next('/');
  } else {
    next();
  }
});

export default router;

Observably, the 'meta: { requiresAuth: true }' is implemented to keep the dashboard route protected against unauthorised users. Writing the router.beforeEach function helps with checkmating if user is authorised when navigating to a protected route.

Also, I required my app to use the router that has been setup, in the root entry of the application (main.js), as below:

//main.js
import './assets/base.css'

import { createApp } from 'vue'
import App from './App.vue'
import router from './router/router'

const app = createApp(App)

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

Finally, I used RouterView to tell my application where to display the routing components. The application layout is written as below:

<!-- appLayout-->
<template>
  <main>
    <Navbar />
    <RouterView />
  </main>
</template>

<script setup>
import Navbar from "@/components/Navbar.vue";
import { RouterView, useRouter } from "vue-router";
</script>

<style scoped>
main {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100vh;
  background: var(--word-color-4);
}
</style>

The authentication layout is as well written as below:

<!-- authLayout-->
<template>
    <main>
        <RouterView />
    </main>
</template>

<script setup>
import { RouterView } from 'vue-router';
</script>

<style scoped>
main {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100vh;
}
</style>

Vuex: Global State Management

I explored vuex for global state management. I stored all data that would be accessed by more than one components in the vuex store (global state). It became easier to access them with predefined functions in vue, such as getters in the vuex store, and computed in the vue component where the data is needed.

See below my vuex store:

//Vuex store
import Vuex from "vuex";
import axios from "axios";


export default new Vuex.Store({
  state: {
    countries: [],
    user: null,
    isOpenForm: true,
  },
  mutations: {
    SET_COUNTRY_DATA(state, countryName) {
      const sortedCountryData = countryName.sort()
      state.countries = sortedCountryData;
      localStorage.setItem("countries", JSON.stringify(sortedCountryData));
    },
    SET_USER_DATA(state, userData) {
      state.user = userData;
      localStorage.setItem("user", JSON.stringify(userData));
      axios.defaults.headers.common.Authorization = `Bearer ${userData.token}`;
    },
    SET_FORM(state, actions) {
      if (actions === "login" || actions === "register") {
        localStorage.setItem("isOpenForm", false);
        state.isOpenForm = false;
      } else {
        localStorage.setItem("isOpenForm", true);
        state.isOpenForm = true;
      }
    },
    CLEAR_USER() {
      localStorage.removeItem("user");
      localStorage.removeItem("isOpenForm");
      localStorage.removeItem("countries");
      location.reload();
    },
  },
  actions: {
    initiateCountryStat: async ({ commit }) => {
      return axios
        .get("https://restcountries.com/v3.1/all")
        .then((response) => {
          const retrievedCountries = response.data.map(b => b.name.common)
          commit("SET_COUNTRY_DATA", retrievedCountries)
        })
        .catch((error) => {
          throw Error(error.response.data.message);
        });
    },
    register: async ({ commit }, credentials) => {
      return axios
        .post("//localhost:8000/register", credentials)
        .then(({ data }) => {
          commit("SET_USER_DATA", data);
          commit("SET_FORM");
        })
        .catch((error) => {
          throw Error(error.response.data.message);
        });
    },
    login: async ({ commit }, credentials) => {
      return axios
        .post("//localhost:8000/login", credentials)
        .then(({ data }) => {
          console.log(data)
          commit("SET_USER_DATA", data);
          commit("SET_FORM");
        })
        .catch((error) => {
          throw Error(error.response.data.message);
        });
    },
    setFormStatus({ commit }, actions) {
      commit("SET_FORM", actions);
    },
    logout: async ({ commit }, credentials) => {
      commit("SET_FORM");
      commit("CLEAR_USER");

      return axios
        .post("//localhost:8000/logout", credentials)
        .catch((error) => {
          throw Error(error.response.data.message);
        });
    },
  },
  getters: {
    getUserData(state) {
      const existingUser = localStorage.getItem("user");
      return existingUser ? true : false
    },
    getUser(state){
      return state.user
    },
    getFormStatus(state) {
      return state.isOpenForm;
    },
    getCountryData(state) {
      return state.countries;
    },
  },
});

Also, I required my app to use the router that has been setup, in the root entry of the application (main.js), as below:

//main.js
import './assets/base.css'

import { createApp } from 'vue'
import App from './App.vue'
import router from './router/router'
import store from './vuex/store'

const app = createApp(App)

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

Server and API Consumption

For API consumption and user data security, I implemented a server side runtime with Nodejs/Express and a simulated database. I'm hoping to implement mongoDB later for the backend.

I installed Axios to manage either HTTP requests from the server and XMLHttp requests from the client.

  • Register User:

    • Server: It receives user credentials, confirms that it isn't conflicting with other user data in the simulated database (user.json), bcrypt user password, generates both access and refresh tokens, and assigns refreshToken to response.cookie, returns user data containing user data and accessToken, except user password and refreshToken which are also stored in the mongoDB database with user data.

    • Client: It makes a post-API call to the server with the user credentials. Upon receiving user data including accessToken, it saves user data to the global state in the vuex store, as well as in the local storage. Also, it assigns the user accessToken to the Axios header authorization as below:

        axios.defaults.headers.common.Authorization = `Bearer ${userData.token}`
      

      Then, the user is permitted to access all protected routes like the dashboard until the token expires.

  • Login User:

    • Server: It receives user credentials, verifies if the user exists in the data by comparing previously bcrypted passwords during registration, generates both access and refresh tokens, assigns refreshToken to response cookie, returns user data containing user data and accessToken, except user password and refreshToken which are also stored in the server simulated database with user data.

    • Client: It makes a post-API call to the server with the user credentials. Upon receiving user data including accessToken, it saves user data to the global state in the Vuex store, as well as in the local storage. Also, it assigns the user accessToken to the Axios header authorization.

      Then, the user is permitted to access all protected routes like the dashboard until the token expires.

Summarily, these were all implementations that went into my holiday project. However, I'm yet to round the application development up for production. But I hope you had fun while reading and understanding why each implementation was very necessary.

I would love to hear your feedback. So, comment below if you have any contribution, criticism, or anything you would want to let me know.

Thanks for reading.