How to Compile Vuejs3 and trying the Composition API

How to Compile Vuejs3 and trying the Composition API

In this post, I’ll compile Vue 3 from the latest source and try out some of the new APIs, include the upcoming composition API, all using TypeScript.

In this post, I’ll compile Vue 3 from the latest source and try out some of the new APIs, include the upcoming composition API, all using TypeScript.

Getting and Compiling Vue 3

To get started with Vue 3, clone the vue-next repo: git clone https://github.com/vuejs/vue-next.git. Now, cd vue-next and install the dependencies by running yarn install, then build the packages by running yarn build. This might take a while.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
  <script src="./packages/vue/dist/vue.global.js"></script>
  <script src="./dist/bundle.js"></script>
</head>
<body>
  <div id="app"></div> 
</body>
</html>

Next, add a basic index.ts:

And index.ts:

export {}

Since we will use TypeScript, we will need a few dependencies and a basic webpack.config.js to compile for use in a browser. Install them:

yarn add yarn add awesome-typescript-loader source-map-loader webpack-cli webpack -W

We need -W since we are currently are in a git repository containing several sub packages (in the packages directory, Vue 3 is very modular like that) and we want to install the dependencies to the top level. We will piggyback of the existing tsconfig.json - but we don't want to watch the entire repository during development, since we have already built the project when we ran yarn build earlier. In the existing tsconfig.json, remove all the entires in the "include" array.

Next, I created a webpack.config.js and added the following:

const path = require('path')
const { CheckerPlugin } = require('awesome-typescript-loader')

module.exports = {
  entry: './index.ts',
  devtool: 'source-map',

  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js'
  },
  module: {
    rules: [
      {
        test: /\.ts/,
        exclude: /node_modules/,
        loader: 'awesome-typescript-loader'
      },
      { enforce: 'pre', test: /\.js$/, loader: 'source-map-loader' }
    ]
  },
  resolve: {
    extensions: ['.ts', '.js']
  },
  plugins: [new CheckerPlugin()]
}

Okay, now we are ready to start hacking. I had bit of trouble getting everything to work — I was really hoping to import a bunch of TypeScript methods and get autocompletion out of the box, but either I didn’t build the packages right or I’m missing something, or it’s just too early for that in development of Vue 3. So we will need some type definitions. The TypeScript experience is, to me, the killer feature of Vue 3, not the new APIs or other features.

Building an App with Vue 3 and TypeScipt

Start by updaing index.ts with the following:

interface IVueNextAPI {
  createApp: () => {
    mount: (appEl: object, selector: string) => void
  }
}

declare global {
  interface Window {
    Vue: IVueNextAPI
  }
}

interface InfoProps {
  count: string
  message: string
}

const Info = {
  setup(props: InfoProps) {
    return props
  },

  template: `
    <div>
      <h3>Info</h3>
      <div>{{ message }}</div>
      <div>Count is: {{ count }}</div>
    </div>
  `
}

interface AppSetup {
  count: number
}

const App = {
  components: {
    Info
  },

  setup(): AppSetup {
    return {
      count: 0
    }
  },

  template: `
    <Info 
      :count="count"
      message="Hello from Vue 3"
    />
  `
}

window.addEventListener('DOMContentLoaded', () => {
  window.Vue.createApp().mount(App, '#app')
})

export {}

I tried using the definitions generated during the yarn build step earlier, but I couldn't get it working - TS errors everywhere, so for now I just made some minimal types to get us started. We no longer do const app = new Vue(....).$mount('#app') anymore - Vue 3 exposes a createApp function, which returns a mount method to which you pass your root component and selector.

Ideally, we would use tsx in render functions, however I couldn't get that working either, so for now I'll just use string literals with template. In the future, tsx and render functions will be supported, and we should be able to get static typechecking on our templates, like in React when using tsx!

You can see there is now a setup function, which takes the props as the first argument, as shown in the Info component. Since App is the top level component, it does not receive any props. setup returns and object which is what would be saved in data and computed fields in the current Vue 2 API. This will still be an option in Vue 3, as well.

Whatever you return from setup are made available in the template function. I like to define this object as XXXSetup, where XXX is the name of component. It's like a schema for your component; it tells the developer what the setup function's API looks like. Think of it like a form a documentation.

setup is called once, when the component is created for the first time (like the existing created lifecycle method). The above code renders the following entirely uninteresting Vue app:

Let’s explore some more of the new APIs.

Using reactive

Vue now exposes a reactive method. This lets us make any object... reactive. The type definition looks like this, taken from here:

interface IVueNextAPI {
  createApp: () => {
    mount: (appEl: object, selector: string) => void
  }

  reactive: <T extends object>(raw: T) => T
}

We can update the setup function in App using the new reactive function. reactive takes an object - count is a primitive. You can use another new API, ref, for primitive values, but for now let's use reactive with a count key. This will make it easy to add more reactive properties soon. They are actually slightly difference, you can read more here in the RFC.

interface AppSetup {
  state: {
    count: number
  }
  increment: () => void
}

const App = {
  components: {
    Info
  },

  setup(): AppSetup {
    const state = window.Vue.reactive({ count: 0 })

    const increment = () => {
      state.count += 1
    }

    return {
      state,
      increment
    }
  },

  template: `
    <Info 
      :count="state.count"
      message="Hello from Vue 3"
      :increment="increment"
    />
  `
}

Next, update Info to use the new increment function:

interface InfoProps {
  count: string
  message: string
  increment: () => void
}

const Info = {
  setup(props: InfoProps) {
    return props
  },

  template: `
    <div>
      <h3>Info</h3>
      <div>{{ message }}</div>
      <div>Count is: {{ count }}</div>
      <button @click="increment">Increment</button>
    </div>
  `
}

Errata: you do not need to _return props_ . Vue automatically makes them available in the template.

Let’s say we want the message prop to be dynamic, based on the value of count. We can use computed. Update the type definition, which in it's basic form is the same as reactive. Also update App:

interface AppSetup {
  state: {
    count: number
  }
  message: () => string
  increment: () => void
}

const App = {
  components: {
    Info
  },

  setup(): AppSetup {
    const state = window.Vue.reactive({ 
      count: 0,
    })

    const increment = () => {
      state.count += 1
    }

    return {
      state,
      increment,
      message: window.Vue.computed(() => `Count is: ${state.count}`)
    }
  },

  template: `
    <Info 
      :count="state.count"
      :message="message"
      :increment="increment"
    />
  `
}

Now we have two count messages that increment together. Let’s take a look at another familiar API from Vue 2, watch. There are quite a few type overloads if you dig deep into the repo, I'll just demonstrate the simplest here.

type StopHandle = () => void

interface Ref<T> {
  value: T
}

type WatcherSource<T> = Ref<T> | (() => T)

interface IVueNextAPI {
  createApp: () => {
    mount: (appEl: object, selector: string) => void
  }

  reactive: <T extends object>(raw: T) => T

  computed: <T extends object>(raw: T) => T

  watch<T>(
    source: WatcherSource<T>,
    effect: (
      value: T,
      oldValue: T,
    ) => void,
  ): StopHandle
}

Final type definitions for this article

interface State {
  count: number
  notifications: Array<{ id: number, content: string }>
}

interface AppSetup {
  state: State
  message: () => string
  increment: () => void
  stop: () => void
}

const App = {
  components: {
    Info
  },

  setup(): AppSetup {
    const state: State = window.Vue.reactive({ 
      count: 0,
      notifications: []
    })

    const increment = () => {
      state.count += 1
    }

    const stop = window.Vue.watch(() => state.count, (val, oldVal) => {
      state.notifications.push(
        {
          id: state.notifications.length + 1,
          content: `${oldVal} => ${val}`
        }
      )
    })


    return {
      state,
      increment,
      stop,
      message: window.Vue.computed(() => `Count is: ${state.count}`)
    }
  },

  template: `
    <Info 
      :count="state.count"
      :notifications="state.notifications"
      :message="message"
      :increment="increment"
      :stop="stop"
    />
  `
}

Final App

watch, takes a function, which should reutrn the reactive object to watch. The next argument is a callback which is called when the watched value mutates. The callback receives the newly updated value and the previous value, just as it does in Vue 2.

interface InfoProps {
  count: string
  message: string
  notifications: Array<{ id: number, content: string }>
  increment: () => void
  stop: () => void
}

const Info = {
  setup(props: InfoProps) {
    return props
  },

  template: `
    <div>
      <h3>Info</h3>
      <div>{{ message }}</div>
      <div>Count is: {{ count }}</div>
      <button @click="increment">Increment</button>
      <h4>Notifications</h4>
      <button @click="stop">Stop</button>
      <ul>
        <li v-for="notification in notifications" :key="notification.id">
          {{ notification.content }}
        </li>
      </ul>
    </div>
  `
}

The final app

This is a pretty basic app, but it does a good job of showing of the new Vue 3 composition API.

Thoughts and Reflection

The new composition API RFC has received mixed feedback from the community. One thing I think is important to remember is the composition API is additive — none of the existing Vue 2 APIs is going away. Here are some of nice things about it, and Vue 3 in general:

  • The entire codebase is in TypeScript, which means we get better type checking and assistance from the IDE.
  • The composition API allows for better Typescript support. I think this is the real killer feature — I don’t have a strong opinion on which API I like better, I just want to have type safety so I can build bug free applications and deliver value to clients.
  • It is certainly a departure from what I first liked about Vue — simple and seemingly “magic” reactivity.
  • Good support for tsx - not my preferred way to write templates, but if it means better type safety, I'll take it.

Some of the cons are:

  • Two ways to write Vue components — just because you know one Vue app well, another might be completely different. This is kind of how I feel about React now. Some codebases use class component with lifecycle methods, and other use function components with the new React hooks. It's a bit tiring to learn so many ways to do the same thing.
  • likely more work to write a plugin supporting two different APIs.
  • You need to learn something new. If you don’t like learning new things, you probably don’t belong in the modern JS world anyway. Not sure if this is a good or bad thing
Conclusion

We explored the new composition API and Vue 3 by compiling it from source. Exciting times are ahead!

Thank you for reading !

How to Run and Use Watchers in Your Vue.js App

How to Run and Use Watchers in Your Vue.js App

Watchers in Vue.js is for watching changes in your component’s variables. It is useful because sometimes we want to do something as some of our components’ variables are in the middle of changing. In this post, we'll learn "How to Run and Use Watchers in Your Vue.js App"

How to Use Watchers in Your Vue.js App

Watchers in Vue.js is for watching changes in your component’s variables. It is useful because sometimes we want to do something as some of our components’ variables are in the middle of changing.

To use Watchers in Vue.js, we put our field into the watch property of our component’s object. When in there, we put our component’s property returned from the data property that we want to watch. For example, if we have a property called form in our data object, then our watch property will look something like:

form: {
  handler(val) {
    if (!this.form) {
      this.toSymbols = this.symbols;
      this.fromSymbols = this.symbols;
      return;
    }
    this.hasResult = false;
    if (this.form.from) {
      this.toSymbols = this.symbols.filter(s => s.code != this.form.from);
    }
    if (this.form.to) {
      this.fromSymbols = this.symbols.filter(s => s.code != this.form.to);
        }
    },
    deep: true
 }

This is an example of a deep watcher, which means changes in all properties in the object are being watched. If anything changes, the handler code will run.

In this story, we will build a currency converter. To get started building our app, we need the Vue CLI, which contains a development server, and scripts to generate boilerplate code. Run npm install -g @vue/cli to install Vue CLI globally.

Then run vue create currency-converter to generate the boilerplate code.

Now we are ready to start writing code. Since we are building a single-page app, we need a router to route URLs to our pages. Vue Router is the most commonly used router for Vue.js apps. In addition, we need form validation and a good looking UI. Vee-validate is a well-known form-validation library for Vue.js apps. It works with many form-validation cases, like checking for required fields and checking for required types of data like numbers.

We also will be displaying graphs in our app, so we want to use a library to make this an easy task. Vue-chartjs is a Vue.js wrapper for the well-known Chart.js library. We also need to get exchange rates from the internet to display and use in our app, so we need an HTTP client. SuperAgent is an HTTP client that fulfills our needs.

After that, we can run the development server to display our app by running npm run serve. It will refresh the browser automatically as we are updating our code.

We can install all of them by running npm i chart.js vue-chartjs vue-material vue-router.

Now that we have all our libraries installed, we can start writing the logic code. First, we start by writing some constants we will use in multiple places by adding a urls.js in the src folder.

Add the following to the file:

const APIURL = 'https://api.exchangeratesapi.io';
export { APIURL };

In main.js, put this code in:

// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import router from './router'router.beforeEach((to, from, next) => {
  document.title = to.meta.title
  next()
})Vue.config.productionTip = false/* eslint-disable no-new */
new Vue({
  el: '#app',
  router,
  template: '<App/>',
  components: { App }
})

This is the entry point of the app.

This block changes the title as we navigate to different pages by listening to Vue router’s navigation events:

router.beforeEach((to, from, next) => {
  document.title = to.meta.title
  next()
})

Also, we need a list of currencies for people to select:

export default {
    "USD": {
        "symbol": "$",
        "name": "US Dollar",
        "symbol_native": "$",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "USD",
        "name_plural": "US dollars"
    },
    "CAD": {
        "symbol": "CA$",
        "name": "Canadian Dollar",
        "symbol_native": "$",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "CAD",
        "name_plural": "Canadian dollars"
    },
    "EUR": {
        "symbol": "€",
        "name": "Euro",
        "symbol_native": "€",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "EUR",
        "name_plural": "euros"
    },
    "AED": {
        "symbol": "AED",
        "name": "United Arab Emirates Dirham",
        "symbol_native": "د.إ.‏",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "AED",
        "name_plural": "UAE dirhams"
    },
    "AFN": {
        "symbol": "Af",
        "name": "Afghan Afghani",
        "symbol_native": "؋",
        "decimal_digits": 0,
        "rounding": 0,
        "code": "AFN",
        "name_plural": "Afghan Afghanis"
    },
    "ALL": {
        "symbol": "ALL",
        "name": "Albanian Lek",
        "symbol_native": "Lek",
        "decimal_digits": 0,
        "rounding": 0,
        "code": "ALL",
        "name_plural": "Albanian lekë"
    },
    "AMD": {
        "symbol": "AMD",
        "name": "Armenian Dram",
        "symbol_native": "դր.",
        "decimal_digits": 0,
        "rounding": 0,
        "code": "AMD",
        "name_plural": "Armenian drams"
    },
    "ARS": {
        "symbol": "AR$",
        "name": "Argentine Peso",
        "symbol_native": "$",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "ARS",
        "name_plural": "Argentine pesos"
    },
    "AUD": {
        "symbol": "AU$",
        "name": "Australian Dollar",
        "symbol_native": "$",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "AUD",
        "name_plural": "Australian dollars"
    },
    "AZN": {
        "symbol": "man.",
        "name": "Azerbaijani Manat",
        "symbol_native": "ман.",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "AZN",
        "name_plural": "Azerbaijani manats"
    },
    "BAM": {
        "symbol": "KM",
        "name": "Bosnia-Herzegovina Convertible Mark",
        "symbol_native": "KM",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "BAM",
        "name_plural": "Bosnia-Herzegovina convertible marks"
    },
    "BDT": {
        "symbol": "Tk",
        "name": "Bangladeshi Taka",
        "symbol_native": "৳",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "BDT",
        "name_plural": "Bangladeshi takas"
    },
    "BGN": {
        "symbol": "BGN",
        "name": "Bulgarian Lev",
        "symbol_native": "лв.",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "BGN",
        "name_plural": "Bulgarian leva"
    },
    "BHD": {
        "symbol": "BD",
        "name": "Bahraini Dinar",
        "symbol_native": "د.ب.‏",
        "decimal_digits": 3,
        "rounding": 0,
        "code": "BHD",
        "name_plural": "Bahraini dinars"
    },
    "BIF": {
        "symbol": "FBu",
        "name": "Burundian Franc",
        "symbol_native": "FBu",
        "decimal_digits": 0,
        "rounding": 0,
        "code": "BIF",
        "name_plural": "Burundian francs"
    },
    "BND": {
        "symbol": "BN$",
        "name": "Brunei Dollar",
        "symbol_native": "$",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "BND",
        "name_plural": "Brunei dollars"
    },
    "BOB": {
        "symbol": "Bs",
        "name": "Bolivian Boliviano",
        "symbol_native": "Bs",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "BOB",
        "name_plural": "Bolivian bolivianos"
    },
    "BRL": {
        "symbol": "R$",
        "name": "Brazilian Real",
        "symbol_native": "R$",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "BRL",
        "name_plural": "Brazilian reals"
    },
    "BWP": {
        "symbol": "BWP",
        "name": "Botswanan Pula",
        "symbol_native": "P",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "BWP",
        "name_plural": "Botswanan pulas"
    },
    "BYR": {
        "symbol": "BYR",
        "name": "Belarusian Ruble",
        "symbol_native": "BYR",
        "decimal_digits": 0,
        "rounding": 0,
        "code": "BYR",
        "name_plural": "Belarusian rubles"
    },
    "BZD": {
        "symbol": "BZ$",
        "name": "Belize Dollar",
        "symbol_native": "$",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "BZD",
        "name_plural": "Belize dollars"
    },
    "CDF": {
        "symbol": "CDF",
        "name": "Congolese Franc",
        "symbol_native": "FrCD",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "CDF",
        "name_plural": "Congolese francs"
    },
    "CHF": {
        "symbol": "CHF",
        "name": "Swiss Franc",
        "symbol_native": "CHF",
        "decimal_digits": 2,
        "rounding": 0.05,
        "code": "CHF",
        "name_plural": "Swiss francs"
    },
    "CLP": {
        "symbol": "CL$",
        "name": "Chilean Peso",
        "symbol_native": "$",
        "decimal_digits": 0,
        "rounding": 0,
        "code": "CLP",
        "name_plural": "Chilean pesos"
    },
    "CNY": {
        "symbol": "CN¥",
        "name": "Chinese Yuan",
        "symbol_native": "CN¥",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "CNY",
        "name_plural": "Chinese yuan"
    },
    "COP": {
        "symbol": "CO$",
        "name": "Colombian Peso",
        "symbol_native": "$",
        "decimal_digits": 0,
        "rounding": 0,
        "code": "COP",
        "name_plural": "Colombian pesos"
    },
    "CRC": {
        "symbol": "₡",
        "name": "Costa Rican Colón",
        "symbol_native": "₡",
        "decimal_digits": 0,
        "rounding": 0,
        "code": "CRC",
        "name_plural": "Costa Rican colóns"
    },
    "CVE": {
        "symbol": "CV$",
        "name": "Cape Verdean Escudo",
        "symbol_native": "CV$",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "CVE",
        "name_plural": "Cape Verdean escudos"
    },
    "CZK": {
        "symbol": "Kč",
        "name": "Czech Republic Koruna",
        "symbol_native": "Kč",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "CZK",
        "name_plural": "Czech Republic korunas"
    },
    "DJF": {
        "symbol": "Fdj",
        "name": "Djiboutian Franc",
        "symbol_native": "Fdj",
        "decimal_digits": 0,
        "rounding": 0,
        "code": "DJF",
        "name_plural": "Djiboutian francs"
    },
    "DKK": {
        "symbol": "Dkr",
        "name": "Danish Krone",
        "symbol_native": "kr",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "DKK",
        "name_plural": "Danish kroner"
    },
    "DOP": {
        "symbol": "RD$",
        "name": "Dominican Peso",
        "symbol_native": "RD$",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "DOP",
        "name_plural": "Dominican pesos"
    },
    "DZD": {
        "symbol": "DA",
        "name": "Algerian Dinar",
        "symbol_native": "د.ج.‏",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "DZD",
        "name_plural": "Algerian dinars"
    },
    "EEK": {
        "symbol": "Ekr",
        "name": "Estonian Kroon",
        "symbol_native": "kr",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "EEK",
        "name_plural": "Estonian kroons"
    },
    "EGP": {
        "symbol": "EGP",
        "name": "Egyptian Pound",
        "symbol_native": "ج.م.‏",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "EGP",
        "name_plural": "Egyptian pounds"
    },
    "ERN": {
        "symbol": "Nfk",
        "name": "Eritrean Nakfa",
        "symbol_native": "Nfk",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "ERN",
        "name_plural": "Eritrean nakfas"
    },
    "ETB": {
        "symbol": "Br",
        "name": "Ethiopian Birr",
        "symbol_native": "Br",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "ETB",
        "name_plural": "Ethiopian birrs"
    },
    "GBP": {
        "symbol": "£",
        "name": "British Pound Sterling",
        "symbol_native": "£",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "GBP",
        "name_plural": "British pounds sterling"
    },
    "GEL": {
        "symbol": "GEL",
        "name": "Georgian Lari",
        "symbol_native": "GEL",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "GEL",
        "name_plural": "Georgian laris"
    },
    "GHS": {
        "symbol": "GH₵",
        "name": "Ghanaian Cedi",
        "symbol_native": "GH₵",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "GHS",
        "name_plural": "Ghanaian cedis"
    },
    "GNF": {
        "symbol": "FG",
        "name": "Guinean Franc",
        "symbol_native": "FG",
        "decimal_digits": 0,
        "rounding": 0,
        "code": "GNF",
        "name_plural": "Guinean francs"
    },
    "GTQ": {
        "symbol": "GTQ",
        "name": "Guatemalan Quetzal",
        "symbol_native": "Q",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "GTQ",
        "name_plural": "Guatemalan quetzals"
    },
    "HKD": {
        "symbol": "HK$",
        "name": "Hong Kong Dollar",
        "symbol_native": "$",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "HKD",
        "name_plural": "Hong Kong dollars"
    },
    "HNL": {
        "symbol": "HNL",
        "name": "Honduran Lempira",
        "symbol_native": "L",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "HNL",
        "name_plural": "Honduran lempiras"
    },
    "HRK": {
        "symbol": "kn",
        "name": "Croatian Kuna",
        "symbol_native": "kn",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "HRK",
        "name_plural": "Croatian kunas"
    },
    "HUF": {
        "symbol": "Ft",
        "name": "Hungarian Forint",
        "symbol_native": "Ft",
        "decimal_digits": 0,
        "rounding": 0,
        "code": "HUF",
        "name_plural": "Hungarian forints"
    },
    "IDR": {
        "symbol": "Rp",
        "name": "Indonesian Rupiah",
        "symbol_native": "Rp",
        "decimal_digits": 0,
        "rounding": 0,
        "code": "IDR",
        "name_plural": "Indonesian rupiahs"
    },
    "ILS": {
        "symbol": "₪",
        "name": "Israeli New Sheqel",
        "symbol_native": "₪",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "ILS",
        "name_plural": "Israeli new sheqels"
    },
    "INR": {
        "symbol": "Rs",
        "name": "Indian Rupee",
        "symbol_native": "টকা",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "INR",
        "name_plural": "Indian rupees"
    },
    "IQD": {
        "symbol": "IQD",
        "name": "Iraqi Dinar",
        "symbol_native": "د.ع.‏",
        "decimal_digits": 0,
        "rounding": 0,
        "code": "IQD",
        "name_plural": "Iraqi dinars"
    },
    "IRR": {
        "symbol": "IRR",
        "name": "Iranian Rial",
        "symbol_native": "﷼",
        "decimal_digits": 0,
        "rounding": 0,
        "code": "IRR",
        "name_plural": "Iranian rials"
    },
    "ISK": {
        "symbol": "Ikr",
        "name": "Icelandic Króna",
        "symbol_native": "kr",
        "decimal_digits": 0,
        "rounding": 0,
        "code": "ISK",
        "name_plural": "Icelandic krónur"
    },
    "JMD": {
        "symbol": "J$",
        "name": "Jamaican Dollar",
        "symbol_native": "$",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "JMD",
        "name_plural": "Jamaican dollars"
    },
    "JOD": {
        "symbol": "JD",
        "name": "Jordanian Dinar",
        "symbol_native": "د.أ.‏",
        "decimal_digits": 3,
        "rounding": 0,
        "code": "JOD",
        "name_plural": "Jordanian dinars"
    },
    "JPY": {
        "symbol": "¥",
        "name": "Japanese Yen",
        "symbol_native": "¥",
        "decimal_digits": 0,
        "rounding": 0,
        "code": "JPY",
        "name_plural": "Japanese yen"
    },
    "KES": {
        "symbol": "Ksh",
        "name": "Kenyan Shilling",
        "symbol_native": "Ksh",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "KES",
        "name_plural": "Kenyan shillings"
    },
    "KHR": {
        "symbol": "KHR",
        "name": "Cambodian Riel",
        "symbol_native": "៛",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "KHR",
        "name_plural": "Cambodian riels"
    },
    "KMF": {
        "symbol": "CF",
        "name": "Comorian Franc",
        "symbol_native": "FC",
        "decimal_digits": 0,
        "rounding": 0,
        "code": "KMF",
        "name_plural": "Comorian francs"
    },
    "KRW": {
        "symbol": "₩",
        "name": "South Korean Won",
        "symbol_native": "₩",
        "decimal_digits": 0,
        "rounding": 0,
        "code": "KRW",
        "name_plural": "South Korean won"
    },
    "KWD": {
        "symbol": "KD",
        "name": "Kuwaiti Dinar",
        "symbol_native": "د.ك.‏",
        "decimal_digits": 3,
        "rounding": 0,
        "code": "KWD",
        "name_plural": "Kuwaiti dinars"
    },
    "KZT": {
        "symbol": "KZT",
        "name": "Kazakhstani Tenge",
        "symbol_native": "тңг.",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "KZT",
        "name_plural": "Kazakhstani tenges"
    },
    "LBP": {
        "symbol": "LB£",
        "name": "Lebanese Pound",
        "symbol_native": "ل.ل.‏",
        "decimal_digits": 0,
        "rounding": 0,
        "code": "LBP",
        "name_plural": "Lebanese pounds"
    },
    "LKR": {
        "symbol": "SLRs",
        "name": "Sri Lankan Rupee",
        "symbol_native": "SL Re",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "LKR",
        "name_plural": "Sri Lankan rupees"
    },
    "LTL": {
        "symbol": "Lt",
        "name": "Lithuanian Litas",
        "symbol_native": "Lt",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "LTL",
        "name_plural": "Lithuanian litai"
    },
    "LVL": {
        "symbol": "Ls",
        "name": "Latvian Lats",
        "symbol_native": "Ls",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "LVL",
        "name_plural": "Latvian lati"
    },
    "LYD": {
        "symbol": "LD",
        "name": "Libyan Dinar",
        "symbol_native": "د.ل.‏",
        "decimal_digits": 3,
        "rounding": 0,
        "code": "LYD",
        "name_plural": "Libyan dinars"
    },
    "MAD": {
        "symbol": "MAD",
        "name": "Moroccan Dirham",
        "symbol_native": "د.م.‏",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "MAD",
        "name_plural": "Moroccan dirhams"
    },
    "MDL": {
        "symbol": "MDL",
        "name": "Moldovan Leu",
        "symbol_native": "MDL",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "MDL",
        "name_plural": "Moldovan lei"
    },
    "MGA": {
        "symbol": "MGA",
        "name": "Malagasy Ariary",
        "symbol_native": "MGA",
        "decimal_digits": 0,
        "rounding": 0,
        "code": "MGA",
        "name_plural": "Malagasy Ariaries"
    },
    "MKD": {
        "symbol": "MKD",
        "name": "Macedonian Denar",
        "symbol_native": "MKD",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "MKD",
        "name_plural": "Macedonian denari"
    },
    "MMK": {
        "symbol": "MMK",
        "name": "Myanma Kyat",
        "symbol_native": "K",
        "decimal_digits": 0,
        "rounding": 0,
        "code": "MMK",
        "name_plural": "Myanma kyats"
    },
    "MOP": {
        "symbol": "MOP$",
        "name": "Macanese Pataca",
        "symbol_native": "MOP$",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "MOP",
        "name_plural": "Macanese patacas"
    },
    "MUR": {
        "symbol": "MURs",
        "name": "Mauritian Rupee",
        "symbol_native": "MURs",
        "decimal_digits": 0,
        "rounding": 0,
        "code": "MUR",
        "name_plural": "Mauritian rupees"
    },
    "MXN": {
        "symbol": "MX$",
        "name": "Mexican Peso",
        "symbol_native": "$",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "MXN",
        "name_plural": "Mexican pesos"
    },
    "MYR": {
        "symbol": "RM",
        "name": "Malaysian Ringgit",
        "symbol_native": "RM",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "MYR",
        "name_plural": "Malaysian ringgits"
    },
    "MZN": {
        "symbol": "MTn",
        "name": "Mozambican Metical",
        "symbol_native": "MTn",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "MZN",
        "name_plural": "Mozambican meticals"
    },
    "NAD": {
        "symbol": "N$",
        "name": "Namibian Dollar",
        "symbol_native": "N$",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "NAD",
        "name_plural": "Namibian dollars"
    },
    "NGN": {
        "symbol": "₦",
        "name": "Nigerian Naira",
        "symbol_native": "₦",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "NGN",
        "name_plural": "Nigerian nairas"
    },
    "NIO": {
        "symbol": "C$",
        "name": "Nicaraguan Córdoba",
        "symbol_native": "C$",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "NIO",
        "name_plural": "Nicaraguan córdobas"
    },
    "NOK": {
        "symbol": "Nkr",
        "name": "Norwegian Krone",
        "symbol_native": "kr",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "NOK",
        "name_plural": "Norwegian kroner"
    },
    "NPR": {
        "symbol": "NPRs",
        "name": "Nepalese Rupee",
        "symbol_native": "नेरू",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "NPR",
        "name_plural": "Nepalese rupees"
    },
    "NZD": {
        "symbol": "NZ$",
        "name": "New Zealand Dollar",
        "symbol_native": "$",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "NZD",
        "name_plural": "New Zealand dollars"
    },
    "OMR": {
        "symbol": "OMR",
        "name": "Omani Rial",
        "symbol_native": "ر.ع.‏",
        "decimal_digits": 3,
        "rounding": 0,
        "code": "OMR",
        "name_plural": "Omani rials"
    },
    "PAB": {
        "symbol": "B/.",
        "name": "Panamanian Balboa",
        "symbol_native": "B/.",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "PAB",
        "name_plural": "Panamanian balboas"
    },
    "PEN": {
        "symbol": "S/.",
        "name": "Peruvian Nuevo Sol",
        "symbol_native": "S/.",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "PEN",
        "name_plural": "Peruvian nuevos soles"
    },
    "PHP": {
        "symbol": "₱",
        "name": "Philippine Peso",
        "symbol_native": "₱",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "PHP",
        "name_plural": "Philippine pesos"
    },
    "PKR": {
        "symbol": "PKRs",
        "name": "Pakistani Rupee",
        "symbol_native": "₨",
        "decimal_digits": 0,
        "rounding": 0,
        "code": "PKR",
        "name_plural": "Pakistani rupees"
    },
    "PLN": {
        "symbol": "zł",
        "name": "Polish Zloty",
        "symbol_native": "zł",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "PLN",
        "name_plural": "Polish zlotys"
    },
    "PYG": {
        "symbol": "₲",
        "name": "Paraguayan Guarani",
        "symbol_native": "₲",
        "decimal_digits": 0,
        "rounding": 0,
        "code": "PYG",
        "name_plural": "Paraguayan guaranis"
    },
    "QAR": {
        "symbol": "QR",
        "name": "Qatari Rial",
        "symbol_native": "ر.ق.‏",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "QAR",
        "name_plural": "Qatari rials"
    },
    "RON": {
        "symbol": "RON",
        "name": "Romanian Leu",
        "symbol_native": "RON",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "RON",
        "name_plural": "Romanian lei"
    },
    "RSD": {
        "symbol": "din.",
        "name": "Serbian Dinar",
        "symbol_native": "дин.",
        "decimal_digits": 0,
        "rounding": 0,
        "code": "RSD",
        "name_plural": "Serbian dinars"
    },
    "RUB": {
        "symbol": "RUB",
        "name": "Russian Ruble",
        "symbol_native": "руб.",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "RUB",
        "name_plural": "Russian rubles"
    },
    "RWF": {
        "symbol": "RWF",
        "name": "Rwandan Franc",
        "symbol_native": "FR",
        "decimal_digits": 0,
        "rounding": 0,
        "code": "RWF",
        "name_plural": "Rwandan francs"
    },
    "SAR": {
        "symbol": "SR",
        "name": "Saudi Riyal",
        "symbol_native": "ر.س.‏",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "SAR",
        "name_plural": "Saudi riyals"
    },
    "SDG": {
        "symbol": "SDG",
        "name": "Sudanese Pound",
        "symbol_native": "SDG",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "SDG",
        "name_plural": "Sudanese pounds"
    },
    "SEK": {
        "symbol": "Skr",
        "name": "Swedish Krona",
        "symbol_native": "kr",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "SEK",
        "name_plural": "Swedish kronor"
    },
    "SGD": {
        "symbol": "S$",
        "name": "Singapore Dollar",
        "symbol_native": "$",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "SGD",
        "name_plural": "Singapore dollars"
    },
    "SOS": {
        "symbol": "Ssh",
        "name": "Somali Shilling",
        "symbol_native": "Ssh",
        "decimal_digits": 0,
        "rounding": 0,
        "code": "SOS",
        "name_plural": "Somali shillings"
    },
    "SYP": {
        "symbol": "SY£",
        "name": "Syrian Pound",
        "symbol_native": "ل.س.‏",
        "decimal_digits": 0,
        "rounding": 0,
        "code": "SYP",
        "name_plural": "Syrian pounds"
    },
    "THB": {
        "symbol": "฿",
        "name": "Thai Baht",
        "symbol_native": "฿",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "THB",
        "name_plural": "Thai baht"
    },
    "TND": {
        "symbol": "DT",
        "name": "Tunisian Dinar",
        "symbol_native": "د.ت.‏",
        "decimal_digits": 3,
        "rounding": 0,
        "code": "TND",
        "name_plural": "Tunisian dinars"
    },
    "TOP": {
        "symbol": "T$",
        "name": "Tongan Paʻanga",
        "symbol_native": "T$",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "TOP",
        "name_plural": "Tongan paʻanga"
    },
    "TRY": {
        "symbol": "TL",
        "name": "Turkish Lira",
        "symbol_native": "TL",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "TRY",
        "name_plural": "Turkish Lira"
    },
    "TTD": {
        "symbol": "TT$",
        "name": "Trinidad and Tobago Dollar",
        "symbol_native": "$",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "TTD",
        "name_plural": "Trinidad and Tobago dollars"
    },
    "TWD": {
        "symbol": "NT$",
        "name": "New Taiwan Dollar",
        "symbol_native": "NT$",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "TWD",
        "name_plural": "New Taiwan dollars"
    },
    "TZS": {
        "symbol": "TSh",
        "name": "Tanzanian Shilling",
        "symbol_native": "TSh",
        "decimal_digits": 0,
        "rounding": 0,
        "code": "TZS",
        "name_plural": "Tanzanian shillings"
    },
    "UAH": {
        "symbol": "₴",
        "name": "Ukrainian Hryvnia",
        "symbol_native": "₴",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "UAH",
        "name_plural": "Ukrainian hryvnias"
    },
    "UGX": {
        "symbol": "USh",
        "name": "Ugandan Shilling",
        "symbol_native": "USh",
        "decimal_digits": 0,
        "rounding": 0,
        "code": "UGX",
        "name_plural": "Ugandan shillings"
    },
    "UYU": {
        "symbol": "$U",
        "name": "Uruguayan Peso",
        "symbol_native": "$",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "UYU",
        "name_plural": "Uruguayan pesos"
    },
    "UZS": {
        "symbol": "UZS",
        "name": "Uzbekistan Som",
        "symbol_native": "UZS",
        "decimal_digits": 0,
        "rounding": 0,
        "code": "UZS",
        "name_plural": "Uzbekistan som"
    },
    "VEF": {
        "symbol": "Bs.F.",
        "name": "Venezuelan Bolívar",
        "symbol_native": "Bs.F.",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "VEF",
        "name_plural": "Venezuelan bolívars"
    },
    "VND": {
        "symbol": "₫",
        "name": "Vietnamese Dong",
        "symbol_native": "₫",
        "decimal_digits": 0,
        "rounding": 0,
        "code": "VND",
        "name_plural": "Vietnamese dong"
    },
    "XAF": {
        "symbol": "FCFA",
        "name": "CFA Franc BEAC",
        "symbol_native": "FCFA",
        "decimal_digits": 0,
        "rounding": 0,
        "code": "XAF",
        "name_plural": "CFA francs BEAC"
    },
    "XOF": {
        "symbol": "CFA",
        "name": "CFA Franc BCEAO",
        "symbol_native": "CFA",
        "decimal_digits": 0,
        "rounding": 0,
        "code": "XOF",
        "name_plural": "CFA francs BCEAO"
    },
    "YER": {
        "symbol": "YR",
        "name": "Yemeni Rial",
        "symbol_native": "ر.ي.‏",
        "decimal_digits": 0,
        "rounding": 0,
        "code": "YER",
        "name_plural": "Yemeni rials"
    },
    "ZAR": {
        "symbol": "R",
        "name": "South African Rand",
        "symbol_native": "R",
        "decimal_digits": 2,
        "rounding": 0,
        "code": "ZAR",
        "name_plural": "South African rand"
    },
    "ZMK": {
        "symbol": "ZK",
        "name": "Zambian Kwacha",
        "symbol_native": "ZK",
        "decimal_digits": 0,
        "rounding": 0,
        "code": "ZMK",
        "name_plural": "Zambian kwachas"
    }
}

We will use this in the currency conversion page.

In index.html, we add:

<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700,400italic|Material+Icons">

This displays Roboto so our app looks good with the Material widgets.

Then in src/router/index.js, we add:

import Vue from 'vue'
import Router from 'vue-router'
import VeeValidate from 'vee-validate';
import Quotes from '@/components/Quotes'
import Convert from '@/components/Convert'
import Drawer from '@/components/Drawer'
import Historical from '@/components/Historical'
import HistoricalRatesInPeriod from '@/components/HistoricalRatesInPeriod'
import HistoricalRatesInPeriodGraph from '@/components/HistoricalRatesInPeriodGraph'
import LineGraph from '@/components/LineGraph'
import currencies from '../currencies';
import VueMaterial from 'vue-material'
import 'vue-material/dist/vue-material.min.css'
import 'vue-material/dist/theme/default.css'Vue.component('drawer', Drawer);
Vue.component('line-graph', LineGraph);
Vue.use(VueMaterial)
Vue.use(Router)
Vue.use(VeeValidate);Vue.filter('currencyName', function (value) {
  if (currencies[value]) {
    return currencies[value].name;
  }
  else {
    return value;
  }})export default new Router({
  routes: [
    {
      path: '/',
      name: 'quotes',
      component: Quotes,
      meta: { title: 'Quotes' }
    },
    {
      path: '/convert',
      name: 'convert',
      component: Convert,
      meta: { title: 'Convert' }
    },
    {
      path: '/historical',
      name: 'historical',
      component: Historical,
      meta: { title: 'Historical Rates' }
    },
    {
      path: '/historicalratesinperiod',
      name: 'historicalratesinperiod',
      component: HistoricalRatesInPeriod,
      meta: { title: 'Historical Rates in a Period' }
    },
    {
      path: '/historicalratesinperiodgraph',
      name: 'historicalratesinperiodgraph',
      component: HistoricalRatesInPeriodGraph,
      meta: { title: 'Historical Rates in a Period' }
    }
  ]
})

This block is where we register our external components with Vue so we can use it in our templates:

Vue.component('drawer', Drawer);
Vue.component('line-graph', LineGraph);
Vue.use(VueMaterial)
Vue.use(Router)
Vue.use(VeeValidate);

Vue also has an element called filter that allows us to map one value to another in our template.

This block gets the currency code and gets the name:

Vue.filter('currencyName', function (value) {
  if (currencies[value]) {
    return currencies[value].name;
  }
  else {
    return value;
  }})

These are the routes for each page of our app. The titles of each page are in the title property of the meta object in each route. The path is the relative URL of each page. We will have to add the components we imported in the file above.

Next, we create the page for the currency-conversion form. We add a file called Convert.vue in thesrc/components folder. The vue extension indicates it is a single-file component, which means that the logic, the style, and the template of the component are all included in one file.

The Convert.vue code looks like this:

<template>
  <div class="page-container md-layout-column">
    <drawer></drawer>
    <div class="container">
      <h1 class="center">Convert Currency</h1>
      <form @submit.prevent="convert">
        <div class="md-layout-item">
          <md-field :class="{'md-invalid': errors.first('price') }">
            <label>Amount of Currency to Convert From</label>
            <md-input v-model="form.price" v-validate="'required'" name="price"></md-input>
            <span class="md-error">{{ errors.first('price') }}</span>
          </md-field>
          <md-field :class="{'md-invalid': errors.first('from') }">
            <label for="from">Currency to Convert From</label>
            <md-select v-model="form.from" name="from" v-validate="'required'">
              <md-option :value="s.code" v-for="s in fromSymbols" :key="s.code">{{s.name}}</md-option>
            </md-select>
            <span class="md-error">{{ errors.first('from') }}</span>
          </md-field>
          <md-field :class="{'md-invalid': errors.first('to') }">
            <label for="to">Currency to Convert To</label>
            <md-select v-model="form.to" name="to" v-validate="'required'">
              <md-option :value="s.code" v-for="s in toSymbols" :key="s.code">{{s.name}}</md-option>
            </md-select>
            <span class="md-error">{{ errors.first('to') }}</span>
          </md-field>
        </div>
        <md-button class="md-dense md-raised md-primary" type="submit">Convert</md-button>
      </form>
      <div v-if="hasResult" class="center">
        <h2
          v-if="isNumber(result)"
        >{{form.price | currencyName}} {{form.from}} is {{result}} {{form.to | currencyName}}</h2>
        <h2 v-if="!isNumber(result)">Exchange rate not found.</h2>
      </div>
    </div>
  </div>
</template><script>
import currencies from "../currencies";
import { APIURL } from "../urls";
import * as moment from "moment";
const request = require("superagent");export default {
  name: "convert",
  data() {
    return {
      form: {
        price: 0,
        from: "",
        to: ""
      },
      result: null,
      currencies,
      currentTime: "",
      err: null,
      hasResult: false,
      symbols: [],
      fromSymbols: [],
      toSymbols: []
    };
  },
  watch: {
    form: {
      handler(val) {
        if (!this.form) {
          this.toSymbols = this.symbols;
          this.fromSymbols = this.symbols;
          return;
        }this.hasResult = false;
        if (this.form.from) {
          this.toSymbols = this.symbols.filter(s => s.code != this.form.from);
        }if (this.form.to) {
          this.fromSymbols = this.symbols.filter(s => s.code != this.form.to);
        }
      },
      deep: true
    }
  },
  methods: {
    convert(evt) {
      this.hasResult = false;
      evt.preventDefault();
      if (this.errors.items.length > 0) {
        return;
      }request.get(`${APIURL}/latest?base=${this.form.from}`).end((err, res) => {
        let body = res.body;
        this.hasResult = true;
        this.result = (+this.form.price * +body.rates[this.form.to]).toFixed(2);
        this.currentTime = moment(this.result.timestamp * 1000).format(
          "YYYY-MM-DD HH:mm:ss a"
        );
      });
    },
    isNumber(number) {
      return !isNaN(number);
    }
  },
  beforeMount() {
    this.symbols = Object.keys(this.currencies).map(k => {
      return {
        symbol: k,
        ...this.currencies[k]
      };
    });
    this.toSymbols = JSON.parse(JSON.stringify(this.symbols));
    this.fromSymbols = JSON.parse(JSON.stringify(this.symbols));
  }
};
</script><!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.result {
  margin-top: 30px;
}
</style>

Note that it uses the currencyName filter that we defined earlier.

The part between the template tags is the form. There is an input and two select dropdowns for the currency we are converting from and to respectively.

In each form field, we check if it has been filled with the v-validate="'required'" attribute. The options are imported from the JSON file with all the currencies. We remove the currency that has already been selected in one of the dropdowns in the watch block in the object that we are exporting in the script tags:

watch: {
    form: {
      handler(val) {
        if (!this.form) {
          this.toSymbols = this.symbols;
          this.fromSymbols = this.symbols;
          return;
        }this.hasResult = false;
        if (this.form.from) {
          this.toSymbols = this.symbols.filter(s => s.code != this.form.from);
        }if (this.form.to) {
          this.fromSymbols = this.symbols.filter(s => s.code != this.form.to);
        }
      },
      deep: true
    }
  },

This makes it so we don’t have the same selection in both dropdowns.

In the code above, we’re watching for changes to all properties of the form object. If the new currency symbol is selected in either the to or from dropdown, then that symbol will be excluded from selection in the to or from field, respectively.

Once the user clicks the submit button, the convert function runs, which gets the exchange rate and computes the converted amount. The this in the object is where the template variables are bound to the variables in the logic. We use moment to format the time, so we have to install that by running npm i moment.

We display the result by assigning to the fields of this. The templates will automatically update.

To create a side menu for navigation, we add a file called Drawer.vue, which we import and register it in index.js so we can include it our template.

The code looks like this:

<template>
  <div>
    <md-drawer :md-active.sync="showNavigation" md-swipeable>
      <md-toolbar class="md-transparent" md-elevation="0">
        <span class="md-title">Currency Info</span>
      </md-toolbar><md-list>
        <md-list-item>
          <router-link to="/">
            <span class="md-list-item-text">Exchange Rates</span>
          </router-link>
        </md-list-item><md-list-item>
          <router-link to="/convert">
            <span class="md-list-item-text">Convert Currency</span>
          </router-link>
        </md-list-item><md-list-item>
          <router-link to="/historical">
            <span class="md-list-item-text">Historical Rates</span>
          </router-link>
        </md-list-item><md-list-item>
          <router-link to="/historicalratesinperiod">
            <span class="md-list-item-text">Historical Rates in a Period</span>
          </router-link>
        </md-list-item><md-list-item>
          <router-link to="/historicalratesinperiodgraph">
            <span class="md-list-item-text">Historical Rates in a Period Graph</span>
          </router-link>
        </md-list-item>
      </md-list>
    </md-drawer>
    <md-toolbar class="md-primary">
      <md-button class="md-icon-button" @click="showNavigation = true">
        <md-icon>menu</md-icon>
      </md-button>
      <h3 class="md-title">Currency Info</h3>
    </md-toolbar>
  </div>
</template><script>
export default {
  name: "nav-bar",
  data() {
    return {
      msg: "Welcome to Your Vue.js App",
      showNavigation: false
    };
  }
};
</script><!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

Now we create the rest of the component files in a similar fashion. They are all in src/components.

We create a file called Historical.vue and add the following:

<template>
  <div class="page-container md-layout-column">
    <drawer></drawer>
    <div class="container">
      <h1 class="center">Historical Exchange Rates</h1>
      <form @submit.prevent="getHistoricalRate">
        <div class="md-layout-item">
          <label>Date</label>
          <md-datepicker
            v-model="form.date"
            v-validate="'required'"
            name="date"
            :md-disabled-dates="futureDates"
          ></md-datepicker>
          <md-field :class="{'md-invalid': errors.first('baseCurrency') }">
            <label for="baseCurrency">Currency Symbol</label>
            <md-select v-model="form.baseCurrency" name="baseCurrency" v-validate="'required'">
              <md-option :value="s.code" v-for="s in symbols" :key="s.code">{{s.name}}</md-option>
            </md-select>
            <span class="md-error">{{ errors.first('baseCurrency') }}</span>
          </md-field>
        </div>
        <md-button class="md-dense md-raised md-primary" type="submit">Find Rate</md-button>
      </form>
      <div>
        <md-content class="md-layout-item" v-for="(r, index) in rates" :key="index">
          <h2>{{form.baseCurrency | currencyName}} - {{r.symbol| currencyName}}</h2>
          <p>{{r.rate}}</p>
        </md-content>
      </div>
    </div>
  </div>
</template><script>
import currencies from "../currencies";
import { APIURL } from "../urls";
import * as moment from "moment";
const request = require("superagent");export default {
  data() {
    return {
      form: {
        date: new Date(),
        baseCurrency: ""
      },
      rates: [],
      symbols: [],
      currencies
    };
  },
  methods: {
    getHistoricalRate(evt) {
      this.hasResult = false;
      evt.preventDefault();
      if (this.errors.items.length > 0) {
        return;
      }request
        .get(
          `${APIURL}/${moment(this.form.date).format("YYYY-MM-DD")}?base=${
            this.form.baseCurrency
          }`
        )
        .end((err, res) => {
          let body = res.body;
          this.hasResult = true;
          this.rates = Object.keys(body.rates).map(k => {
            return {
              symbol: k,
              rate: body.rates[k]
            };
          });
        });
    },
    futureDates(evt) {
      return +evt > +new Date();
    }
  },
  beforeMount() {
    this.symbols = Object.keys(this.currencies).map(k => {
      return {
        symbol: k,
        ...this.currencies[k]
      };
    });
  }
};
</script><style>
</style>

In the component, thegetHistoricalRate function runs code to make a GET request to the API to exchange with our Superagent HTTP client library. After the request is done, the then function that is chained to the getfunction takes the exchange rate result and does calculations.

Then we create HistoricalRatesInPeriod.vue in the same folder and add the following:

<template>
  <div class="page-container md-layout-column">
    <drawer></drawer>
    <div class="container">
      <h1 class="center">Historical Exchange Rates in a Period</h1>
      <form @submit.prevent="getHistoricalRate">
        <div class="md-layout-item">
          <label>Start Date</label>
          <md-datepicker
            v-model="form.startDate"
            v-validate="'required'"
            name="date"
            :md-disabled-dates="invalidStartDates"
          ></md-datepicker>
          <label>End Date</label>
          <md-datepicker
            v-model="form.endDate"
            v-validate="'required'"
            name="date"
            :md-disabled-dates="invalidEndDates"
          ></md-datepicker>
          <md-field :class="{'md-invalid': errors.first('baseCurrency') }">
            <label for="baseCurrency">Currency Symbol</label>
            <md-select v-model="form.baseCurrency" name="baseCurrency" v-validate="'required'">
              <md-option :value="s.code" v-for="s in symbols" :key="s.code">{{s.name}}</md-option>
            </md-select>
            <span class="md-error">{{ errors.first('baseCurrency') }}</span>
          </md-field>
        </div>
        <md-button class="md-dense md-raised md-primary" type="submit">Find Rate</md-button>
      </form>
      <div>
        <md-content class="md-layout-item" v-for="(r, index) in rates" :key="index">
          <h2>{{r.date}}</h2>
          <md-content class="md-layout-item" v-for="(rr, index) in r.rates" :key="index">
            <h3>{{form.baseCurrency | currencyName}} - {{rr.symbol| currencyName}}</h3>
            <p>{{rr.rate}}</p>
          </md-content>
        </md-content>
      </div>
    </div>
  </div>
</template><script>
import currencies from "../currencies";
import { APIURL } from "../urls";
import * as moment from "moment";
const request = require("superagent");export default {
  data() {
    return {
      form: {
        startDate: new Date(),
        endDate: new Date(),
        baseCurrency: ""
      },
      rates: [],
      symbols: [],
      currencies
    };
  },
  methods: {
    getHistoricalRate(evt) {
      this.hasResult = false;
      evt.preventDefault();
      if (this.errors.items.length > 0) {
        return;
      }request
        .get(
          `${APIURL}/history?start_at=${moment(this.form.startDate).format(
            "YYYY-MM-DD"
          )}&end_at=${moment(this.form.endDate).format("YYYY-MM-DD")}&base=${
            this.form.baseCurrency
          }`
        )
        .end((err, res) => {
          let body = res.body;
          this.hasResult = true;
          this.rates = Object.keys(body.rates)
            .map(date => {
              return {
                date: moment(date).format("YYYY-MM-DD"),
                rates: Object.keys(body.rates[date]).map(k => {
                  return {
                    symbol: k,
                    rate: body.rates[date][k]
                  };
                })
              };
            })
            .sort((a, b) => +new Date(a.date) - +new Date(b.date));
        });
    },
    invalidStartDates(evt) {
      return +evt > +new Date();
    },
    invalidEndDates(evt) {
      return +evt < +this.form.startDate || +evt > +new Date();
    }
  },
  beforeMount() {
    this.symbols = Object.keys(this.currencies).map(k => {
      return {
        symbol: k,
        ...this.currencies[k]
      };
    });
  }
};
</script><style>
</style>

The date picker and other components are all registered in index.js so we can include them in our templates.

Next, we add a page to “Historical Exchange Rates in a Period” in a graph by adding a file called HistoreicalRatesInPeriodGraph.vue and add the following code:

<template>
  <div class="page-container md-layout-column">
    <drawer></drawer>
    <div class="container">
      <h1 class="center">Historical Exchange Rates in a Period Graph</h1>
      <form @submit.prevent="getHistoricalRate">
        <div class="md-layout-item">
          <label>Start Date</label>
          <md-datepicker
            v-model="form.startDate"
            v-validate="'required'"
            name="date"
            :md-disabled-dates="invalidStartDates"
          ></md-datepicker>
          <label>End Date</label>
          <md-datepicker
            v-model="form.endDate"
            v-validate="'required'"
            name="date"
            :md-disabled-dates="invalidEndDates"
          ></md-datepicker>
          <md-field :class="{'md-invalid': errors.first('from') }">
            <label for="from">Currency to Convert From</label>
            <md-select v-model="form.from" name="from" v-validate="'required'">
              <md-option :value="s.code" v-for="s in fromSymbols" :key="s.code">{{s.name}}</md-option>
            </md-select>
            <span class="md-error">{{ errors.first('from') }}</span>
          </md-field>
          <md-field :class="{'md-invalid': errors.first('to') }">
            <label for="to">Currency to Convert To</label>
            <md-select v-model="form.to" name="to" v-validate="'required'">
              <md-option :value="s.code" v-for="s in toSymbols" :key="s.code">{{s.name}}</md-option>
            </md-select>
            <span class="md-error">{{ errors.first('to') }}</span>
          </md-field>
        </div>
        <md-button class="md-dense md-raised md-primary" type="submit">Find Historical Rates</md-button>
      </form>
      <div>
        <line-graph v-if="hasResult && !resultNotFound" :chartdata="graphData" :options="options"></line-graph>
        <h2 v-if="hasResult && resultNotFound">Exchange rates not found.</h2>
      </div>
    </div>
  </div>
</template><script>
import currencies from "../currencies";
import { APIURL } from "../urls";
import * as moment from "moment";
const request = require("superagent");export default {
  data() {
    return {
      form: {
        startDate: new Date(),
        endDate: new Date(),
        from: "",
        to: ""
      },
      rates: [],
      symbols: [],
      fromSymbols: [],
      toSymbols: [],
      currencies,
      graphData: {},
      options: {
        responsive: true,
        maintainAspectRatio: false
      },
      hasResult: false,
      resultNotFound: false
    };
  },
  watch: {
    form: {
      handler(val) {
        if (!this.form) {
          this.toSymbols = this.symbols;
          this.fromSymbols = this.symbols;
          return;
        }
        
        if (this.form.from) {
          this.toSymbols = this.symbols.filter(s => s.code != this.form.from);
        }if (this.form.to) {
          this.fromSymbols = this.symbols.filter(s => s.code != this.form.to);
        }
      },
      deep: true
    }
  },
  methods: {
    getHistoricalRate(evt) {
      this.hasResult = false;
      evt.preventDefault();
      if (this.errors.items.length > 0) {
        return;
      }request
        .get(
          `${APIURL}/history?start_at=${moment(this.form.startDate).format(
            "YYYY-MM-DD"
          )}&end_at=${moment(this.form.endDate).format("YYYY-MM-DD")}&base=${
            this.form.from
          }`
        )
        .end((err, res) => {
          let body = res.body;
          this.hasResult = true;
          if (!body.rates) {
            this.resultNotFound = true;
            return;
          } else {
            this.resultNotFound = false;
          }
          let rates = Object.keys(body.rates)
            .map(date => {
              return {
                date: moment(date).format("YYYY-MM-DD"),
                rates: Object.keys(body.rates[date]).map(k => {
                  return {
                    symbol: k,
                    rate: body.rates[date][k]
                  };
                })
              };
            })
            .sort((a, b) => +new Date(a.date) - +new Date(b.date));let exchangeRates = [];
          rates.forEach(r => {
            let currency = r.rates.find(rr => rr.symbol == this.form.to);
            let rate;
            if (currency) {
              rate = currency.rate;
            }
            if (rate) {
              exchangeRates.push(rate);
            }
          });this.graphData = {
            labels: rates.map(r => r.date),
            datasets: [
              {
                label: `Exchange Rate ${this.form.from} - ${this.form.to}`,
                backgroundColor: "#f87979",
                data: exchangeRates,
                fill: false
              }
            ]
          };if (exchangeRates.length == 0) {
            this.resultNotFound = true;
          }
        });
    },
    invalidStartDates(evt) {
      return +evt > +new Date();
    },
    invalidEndDates(evt) {
      return +evt < +this.form.startDate || +evt > +new Date();
    }
  },
  beforeMount() {
    this.symbols = Object.keys(this.currencies).map(k => {
      return {
        symbol: k,
        ...this.currencies[k]
      };
    });
    this.toSymbols = JSON.parse(JSON.stringify(this.symbols));
    this.fromSymbols = JSON.parse(JSON.stringify(this.symbols));
  }
};
</script><style>
</style>

To display our line graph in our app, we have to create a component required by Vue Chart.js. Make a file calledLineGraph.vue, and add the following:

<script>
import { Line } from 'vue-chartjs'export default {
  extends: Line,
  props: ['chartdata', 'options'],
  mounted () {
    this.renderChart(this.chartdata, this.options)
  }
}
</script><style>
</style>

Then we make a page to display our quotes. Call it Quotes.vue, and add the following:

<template>
  <div class="page-container md-layout-column">
    <drawer></drawer>
    <md-content>
      <h1 class="center">Quotes as of {{currentTime}}</h1>
      <div class="container">
        <div class="md-layout-item">
          <md-field>
            <label for="movie">Base Currency</label>
            <md-select v-model="baseCurrency" name="baseCurrency">
              <md-option value="EUR">Euro</md-option>
              <md-option value="USD">US Dollar</md-option>
              <md-option value="CAD">Canadian Dollar</md-option>
            </md-select>
          </md-field>
        </div>
        <div>
          <md-content class="md-layout-item" v-for="(q, index) in quoteData.rates" :key="index">
            <h2>{{quoteData.base}} - {{q.symbol| currencyName}}</h2>
            <p>Price: {{q.rate}}</p>
          </md-content>
        </div>
      </div>
    </md-content>
  </div>
</template><script>
import currencies from "../currencies";
import { APIURL } from "../urls";
import * as moment from "moment";
const request = require("superagent");export default {
  name: "quotes",
  data() {
    return {
      quoteData: {},
      currentTime: moment().format("YYYY-MM-DD HH:mm:ss a"),
      baseCurrency: null,
      showNavigation: false
    };
  },
  methods: {
    getQuotes() {
      let url = `${APIURL}/latest`;
      if (this.baseCurrency) {
        url = `${APIURL}/latest?base=${this.baseCurrency}`;
      }
      request.get(url).end((err, res) => {
        this.quoteData = res.body;
        this.quoteData.rates = Object.keys(this.quoteData.rates).map(k => {
          return {
            symbol: k,
            rate: this.quoteData.rates[k]
          };
        });
        this.currentTime = moment().format("YYYY-MM-DD HH:mm:ss a");
      });
    }
  },
  beforeMount() {
    this.getQuotes();
  },
  watch: {
    baseCurrency: function() {
      this.getQuotes();
    }
  }
};
</script><!-- Add "scoped" attribute to limit CSS to this component only -->
<style></style>

In the end, we get the working currency converter app written in Vue:

To build our app for production deployment, run npm run build.

Thank for visiting and reading this article! I'm highly appreciate your actions! Please share if you liked it!

Building a Modern App using Nest.js, MongoDB and Vue.js

Building a Modern App using Nest.js, MongoDB and Vue.js

In this Node.js tutorial, you'll learn how to build a modern app using Nest.js, MongoDB and Vue.js. Learn how to create a simple customer list management application by using Nest.js and Vue.js. Here, you use Nest.js to build a RESTful backend API and then leverage on Vue.js to craft a client that consumes the API.

In this Node.js tutorial, you'll learn how to build a modern app using Nest.js, MongoDB and Vue.js. Learn how to create a simple customer list management application by using Nest.js and Vue.js. Here, you use Nest.js to build a RESTful backend API and then leverage on Vue.js to craft a client that consumes the API.

Table of Contents

  • Introduction
  • Prerequisites
  • Why Nest and Vue
  • What you’ll build
  • Installing Nest.js and its dependencies
  • Start the application
  • Connecting with MongoDB database
  • Setting up and configuring a database schema, interfaces and DTO
  • Database schema
  • Interfaces
  • Data transfer object (DTO)
  • Creating module, controller and service for the application
  • Generate modules
  • Generate service
  • Generate controller
  • Update the customer module
  • Enable CORS
  • Creating the frontend app with Vue.js
  • Run the Vue.js app
  • Install axios
  • Create reusable components
  • Create customer component
  • Edit component
  • View all customer
  • Update App.vue
  • Include Bootstrap
  • Setting up routing
  • Test the application
  • Create new customer
  • Homepage ( View all customers )
  • Edit a customer details
  • Conclusion
Introduction

Nest.js introduces a modern way of building Node.js apps by giving it a proper and modular structure out of the box. It was fully built with TypeScript but still preserves compatibility with plain JavaScript.

In this post, I will introduce and explain the fundamental steps to follow in order to combine this awesome framework with a modern frontend JavaScript framework such as Vue.js. You will build a web application to manage customers information. The application will be used to create a new customer, add several details about the customer and update each customer’s records in the database.

The approach to this post will be to build a separate REST API backend with Nest.js and a frontend to consume this API using Vue.js. So basically, instead of building Nest.js application that uses a Node.js template engine for the client side, you will leverage on the awesomeness of Vue.js as a progressive JavaScript library to quickly render contents and handled every client-side related logic.

In addition, you will use MongoDB database to persist and retrieve data for the application. MongoDB is a schema-less NoSQL document database that can receive and store data in JSON document format. It is often use with Mongoose; an Object Data Modeling (ODM) library, that helps to manage relationships between data and provides schema validations. You learn more about this later in this tutorial.

Prerequisites
  • A reasonable knowledge of building applications with JavaScript is required and a basic knowledge of TypeScript will be an added advantage.

  • Ensure that you have Node and npm installed on your local system. Check this link for Node and here for instructions on how to install npm.

  • Install MongoDB on your local system. Follow the instructions here to download and installed it for your choice of operating system. This tutorial uses MacOS machine for development. To successfully install MongoDB, you can either install it by using homebrew on Mac or by downloading it from the MongoDB website.

  • A text editor installed, such as Visual Studio Code, Atom, or Sublime Text

Why Nest and Vue

Nest.js has a reputation of bringing design patterns and mature structure to node.js world. This makes it quite easy to use it as the right tool for building awesome web applications. For the fact that Nest.js uses express library under the hood.

Nest.js is fully featured and structured to support MVC (Model-View-Controller) design pattern.
This means, you can install one of the popular template engine used in node.js and configure it to handle the flow of the application and interaction with backend API from the front end.
While this might be sufficient for a small app, it is always better to consider a much better and contemporary approach to handling frontend related part of an application by leveraging on a tool like Vue.js. Vue can be used to set up the frontend logic of your application as you will see later in this post.
Vue.js is a progressive javaScript framework for building reusable components for user interfaces. It is simple and yet very powerful and ideal for any project. This makes it seamless to use for a Nest.js application for example.

As you proceed in this tutorial, you will learn how to use and successfully combine these two tools, that is, Nest.js and Vue.js to build highly interactive web app.

What you’ll build

As mentioned earlier in this post, you will build a customer list management application. To keep things really simple here, we will not be implementing authentication and authorization for any user. The main objective is for you to get conversant and comfortable using both Nest.js and Vue.js. At the end of the day, you would have learnt means to craft and structure this application as shown below:

We will use Nest.js to develop the backend API and then a Vue.js application to build components for creating, editing, deleting and showing the total list of customers from a mongoDB database.

Installing Nest.js and its dependencies

Now that the basic introductory contents have been properly covered, you will proceed to installing Nest.js and its required dependencies. Getting Nest.js installed can be done in two different ways:

  • Scaffold the base project with Nest CLI tool
  • Installing the starter project on GitHub by using Git

You will use the Nest CLI here in this tutorial to easily craft a new Nest.js project. This comes with a lot of benefits like scaffolding a new project seamlessly, generating different files by using the nest command amongst other things.

First, you need to install the CLI globally on your system. To do that, run the following command from the terminal:

npm install -g @nestjs/cli

The installation of this tool will give you access to the nest command to manage the project and create Nest.js application related files as you will see later in this post.

Now that you have the CLI installed, you can now proceed to create the project for this tutorial by running the following command from your local development folder:

nest new customer-list-app-backend

The preceding command will generate a customer-list-app-backend application. Next, change directory into this new project and install the only server dependency for the backend. As mentioned earlier, you will use MongoDB database to persist data for the application. To integrate it with a Nest.js project, you need to install mongoose and the mongoose package built by the Nest.js team. Use the following command for this purpose:

 cd customer-list-app-backend

 npm install --save @nestjs/mongoose mongoose

Start the application

With the installation process properly covered, you can now start the application with:

npm run start

This will start the application on the default port of 3000. Navigate to http://localhost:3000 from your favorite browser and you should have a page similar to this:

Connecting with MongoDB database

It is assumed that by now, you have installed MongoDB on your machine as instructed at the beginning of this post. To start the database, open a new terminal so that the application keeps running and run sudo mongod . The preceding command will start the MongoDB service and simultaneously run the database in the background.

Next, to quickly setup a connection to the database from your application, you will have to import the installed MongooseModule within the root ApplicationModule. To do this, use your preferred code editor to open the project and locate ./src/app.module.ts. Update the contents with the following:

// ./src/app.module.ts

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { MongooseModule } from '@nestjs/mongoose';
@Module({
  imports: [
    MongooseModule.forRoot('mongodb://localhost/customer-app', { useNewUrlParser: true })
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Here, Mongoose module for MongoDB uses the forRoot() method to supply the connection to the database.

Setting up and configuring a database schema, interfaces and DTO

To properly structure the kind of data that will be stored in the database for this application, you will create a database schema, a TypeScript and a data transfer object (DTO).

Database schema

Here, you will create a mongoose database schema that will determine the data that should be stored in the database. To begin, navigate to the ./src/ folder and first, create a new folder named customer and within the newly created folder, create another one and call it schemas. Now create a new file within the schemas and named customer.schema.ts . Open this newly created file and paste the following code in it:

// ./src/customer/schemas/customer.schema.ts
import * as mongoose from 'mongoose';

export const CustomerSchema = new mongoose.Schema({
    first_name: String,
    last_name: String,
    email: String,
    phone: String,
    address: String,
    description: String,
    created_at: { type: Date, default: Date.now }
})

This will ensure that data with string value will be stored in the database.

Interfaces

Next, you will create a TypeScript interface which will be used for type-checking and to determine the type of values that will be received by the application. To set it up, create a new folder named interfaces within the ./src/customer folder. After that, create a new file within the newly created folder and name it customer.interface.ts . Paste the following code in it :

// ./src/customer/interfaces/customer.interface.ts
import { Document } from 'mongoose';

export interface Customer extends Document {
    readonly first_name: string;
    readonly last_name: string;
    readonly email: string;
    readonly phone: string;
    readonly address: string;
    readonly description: string;
    readonly created_at: Date;
}

Data transfer object (DTO)

A data transfer object will define how data will be sent on over the network. To do this, create a folder dto inside ./src/customer folder and create a new file create-customer.dto.ts and paste the code below in it:

// ./src/customer/dto/create-customer.dto.ts
export class CreateCustomerDTO {
    readonly first_name: string;
    readonly last_name: string;
    readonly email: string;
    readonly phone: string;
    readonly address: string;
    readonly description: string;
    readonly created_at: Date;
}

You are now done with the basic configurations of connecting and interacting with the database

Creating module, controller and service for the application

Generate modules

A module in Nest.js is identified by the @Module() decorator and it takes in objects such as controllers and providers. Here you will leverage on the nest command to easily generate a new module for the customer app. This will ensure that the application is properly structured and more organized. Stop the application, if it is currently running with CTRL + C and run the following command

nest generate module customer

This will create a new file named customer.module.ts within the src/customer folder and update the root module (i.e app.module.ts) of the application with the details of the newly created module.

// ./src/customer/customer.module.ts

import { Module } from '@nestjs/common';
@Module({})
export class CustomerModule {}

You will come back to add more content to this module later in this post.

Generate service

Service also known as provider in Nest.js basically carry out the task of abstracting logic away from controllers. With it in place, a controller will only carry out the functionality of handling HTTP requests from the frontend and delegate the complex tasks to services. Service or provider in Nest.js is identified by adding @Injectable() decorator on top of them.

Generate a new service using the nest command by running the following command from the terminal within the project directory:

nest generate service customer

After successfully running the command above, two new files will be created. They are:

  • customer.service.ts: this is the main service file with @Injectable() decorator
  • customer.service.spec.ts: a file for unit testing. You can ignore this file for now as testing will not be covered in this tutorial.

The customer.service.ts file holds all the logic as regards database interaction for creating and updating every details of a new customer. In a nutshell, the service will receive a request from the controller, communicate this to the database and return an appropriate response afterwards.

Open the newly created customer.service.ts file and replace the existing code with the following :

// ./src/customer/customer.service.ts
import { Injectable } from '@nestjs/common';
import { Model } from 'mongoose';
import { InjectModel } from '@nestjs/mongoose';
import { Customer } from './interfaces/customer.interface';
import { CreateCustomerDTO } from './dto/create-customer.dto';

@Injectable()
export class CustomerService {
    constructor(@InjectModel('Customer') private readonly customerModel: Model<Customer>) { }
    // fetch all customers
    async getAllCustomer(): Promise<Customer[]> {
        const customers = await this.customerModel.find().exec();
        return customers;
    }
    // Get a single customer
    async getCustomer(customerID): Promise<Customer> {
        const customer = await this.customerModel.findById(customerID).exec();
        return customer;
    }
    // post a single customer
    async addCustomer(createCustomerDTO: CreateCustomerDTO): Promise<Customer> {
        const newCustomer = await this.customerModel(createCustomerDTO);
        return newCustomer.save();
    }
    // Edit customer details
    async updateCustomer(customerID, createCustomerDTO: CreateCustomerDTO): Promise<Customer> {
        const updatedCustomer = await this.customerModel
            .findByIdAndUpdate(customerID, createCustomerDTO, { new: true });
        return updatedCustomer;
    }
    // Delete a customer
    async deleteCustomer(customerID): Promise<any> {
        const deletedCustomer = await this.customerModel.findByIdAndRemove(customerID);
        return deletedCustomer;
    }
}

Here, you imported the required module from @nestjs/common, mongoose and @nestjs/mongoose. In addition, you also imported the interface created earlier named Customer and a data transfer object CreateCustomerDTO.

In order to be able to seamlessly carry out several database related activities such as, creating a customer, retrieving the list of customers or just a single customer, you used the @InjectModel method to inject the Customer model into the CustomerService class.

Next, you created the following methods:

  • getAllCustomer() : to retrieve and return the list of customers from the database
  • getCustomer(): it takes customerID as a parameter and based on that, it will search and return the details of a user identified by that ID.
  • addCustomer(): used to add a new customer to the database
  • updateCustomer(): this method also takes the ID of a customer as an argument and will be used to edit and update the details of such customer in the database.
  • deleteCustomer(): this will be used to delete the details of a particular customer completely from the database.

Generate controller

Handling each route within the application is one of the major responsibility of controllers in Nest.js. Similar to most JavaScript server-side framework for the web, several endpoints will be created and any requests sent to such endpoint from the client side will be mapped to a specific method within the controller and an appropriate response will be returned.

Again, you will use the nest command to generate the controller for this application. To achieve that, run the following command:

nest generate controller customer

This command will also generate two new files within the src/customer, they are , customer.controller.spec.ts and customer.controller.ts files respectively. The customer.controller.ts file is the actual controller file and the second one should be ignored for now. Controllers in Nest.js are TypeScript files decorated with @Controller metadata.

Now open the controller and replace the content with the following code that contains methods to create a new customer, retrieve the details of a particular customer and fetch the list of all customers from the database:

// ./src/customer/customer.controller.ts
import { Controller, Get, Res, HttpStatus, Post, Body, Put, Query, NotFoundException, Delete, Param } from '@nestjs/common';
import { CustomerService } from './customer.service';
import { CreateCustomerDTO } from './dto/create-customer.dto';

@Controller('customer')
export class CustomerController {
    constructor(private customerService: CustomerService) { }

    // add a customer
    @Post('/create')
    async addCustomer(@Res() res, @Body() createCustomerDTO: CreateCustomerDTO) {
        const customer = await this.customerService.addCustomer(createCustomerDTO);
        return res.status(HttpStatus.OK).json({
            message: "Customer has been created successfully",
            customer
        })
    }

    // Retrieve customers list
    @Get('customers')
    async getAllCustomer(@Res() res) {
        const customers = await this.customerService.getAllCustomer();
        return res.status(HttpStatus.OK).json(customers);
    }

    // Fetch a particular customer using ID
    @Get('customer/:customerID')
    async getCustomer(@Res() res, @Param('customerID') customerID) {
        const customer = await this.customerService.getCustomer(customerID);
        if (!customer) throw new NotFoundException('Customer does not exist!');
        return res.status(HttpStatus.OK).json(customer);
    }
}

In order to interact with the database, the CustomerService was injected into the controller via the class constructor(). The addCustomer() and getAllCustomer() methods will be used to add a new customer’s details and retrieve the list of customers while the getCustomer() receives the customerID as a query parameter and throw an exception if the customer does not exist in the database.

Next, you need to be able to update and delete the details of a customer where and when necessary. For this you will add two more methods to the CustomerController class. Open the file again and add this:

// ./src/customer/customer.controller.ts
...
@Controller('customer')
export class CustomerController {
    constructor(private customerService: CustomerService) { }
    ...

    // Update a customer's details
    @Put('/update')
    async updateCustomer(@Res() res, @Query('customerID') customerID, @Body() createCustomerDTO: CreateCustomerDTO) {
        const customer = await this.customerService.updateCustomer(customerID, createCustomerDTO);
        if (!customer) throw new NotFoundException('Customer does not exist!');
        return res.status(HttpStatus.OK).json({
            message: 'Customer has been successfully updated',
            customer
        });
    }

    // Delete a customer
    @Delete('/delete')
    async deleteCustomer(@Res() res, @Query('customerID') customerID) {
        const customer = await this.customerService.deleteCustomer(customerID);
        if (!customer) throw new NotFoundException('Customer does not exist');
        return res.status(HttpStatus.OK).json({
            message: 'Customer has been deleted',
            customer
        })
    }
}

Update the customer module

To keep things properly organised go back to the customer.module.ts and set up the Customer model. Update the content with the following:

// ./src/customer/customer.module.ts

import { Module } from '@nestjs/common';
import { CustomerController } from './customer.controller';
import { CustomerService } from './customer.service';
import { MongooseModule } from '@nestjs/mongoose';
import { CustomerSchema } from './schemas/customer.schema';
@Module({
  imports: [
    MongooseModule.forFeature([{ name: 'Customer', schema: CustomerSchema }])
  ],
  controllers: [CustomerController],
  providers: [CustomerService]
})
export class CustomerModule { }

Enable CORS

By default, it is forbidden for two separate application on different ports to interact or share resources with each other unless it is otherwise allowed by one of them, which is often the server-side. In order to allow request from the client side that will be built with Vue.js, you will need to enable CORS (Cross-Origin Resource Sharing).

To do that in Nest.js, you only need to add app.enableCors() to the main.ts file as shown below:

// ./src/main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.enableCors(); // add this line
  await app.listen(3000);
}
bootstrap();

With this, you have just completed the backend part of the application and can now proceed to build the frontend with Vue.js

Creating the frontend app with Vue.js

The team at Vue.js already created an awesome tool named Vue CLI. It is a standard tool that allows you to quickly generate and install a new Vue.js project with ease. You will use that here to create the frontend part of the customer app, but first you need to install Vue CLI globally on your machine.

Open a new terminal and run:

npm install -g @vue/cli

Once the installation process is complete, you can now use the vue create command to craft a new Vue.js project. Run the following command to do that for this project:

vue create customer-list-app-frontend

Immediately after you hit return, you will be prompted to pick a preset. You can choose manually select features:

Next, check the features you will need for this project by using the up and down arrow key on your keyboard to navigate through the list of features. Press the spacebar to select a feature from the list. Select Babel, Router and Linter / Formatter as shown here:

Hitting return here will show you another list of options For other instructions, type y to use history mode for a router, this will ensure that history mode is enabled within the router file that will automatically be generated for this project.

Next, select ESLint with error prevention only in order to pick a linter / formatter config. After that, select Lint on save for additional lint features and save your configuration in a dedicated config file for future projects. Type a name for your preset, I named mine vuescotch:

This will create a Vue.js application in a directory named customer-list-app-frontend and install all its required dependencies.

Run the Vue.js app

You can now change directory into the newly created project and start the application with:

// change directory
cd customer-list-app-frontend

// run the application
npm run server

You can now view the application on http://localhost:8080

Install axios

Axios, a promised based HTTP client for browser will be used here to perform HTTP requests from different component within the application. Stop the frontend application from running by hitting CTRL + C from the terminal and run the following command afterwards:

npm install axios --save

Once the installation process is completed, open the customer-list-app-frontend within a code editor and create a new file named helper.js within the src folder. Open the newly created file and paste the following content in it:

// ./src/helper.js
export const server = {
    baseURL: 'http://localhost:3000'
}

What you have done here is to define the baseURL for the backend project built with Nest.js. This is just to ensure that you don’t have to start declaring this URL within several Vue.js components that you will create in the next section.

Create reusable components

Vue.js favours building and structuring applications by creating reusable components to give it a proper structure. Vue.js components contains three different sections, which are

  • <template></template>
  • <script></script>
  • <style></style>.
Create customer component

You will start by creating a component within the application for a user to create a customer. This component will contain a form with few input fields required to accepts details of a customer and once the form is submitted, the details from the input fields will be posted to the server. To achieve this, create a new folder named customer within the ./src/components folder. This newly created folder will house all the components for this application. Next, create another file within the customer folder and name it Create.vue. Open this new file and add the following:

// ./src/components/customer/Create.vue

<template>
   <div>
        <div class="col-md-12 form-wrapper">
          <h2> Create Customer </h2>
          <form id="create-post-form" @submit.prevent="createCustomer">
               <div class="form-group col-md-12">
                <label for="title"> First Name </label>
                <input type="text" id="first_name" v-model="first_name" name="title" class="form-control" placeholder="Enter firstname">
               </div>
               <div class="form-group col-md-12">
                <label for="title"> Last Name </label>
                <input type="text" id="last_name" v-model="last_name" name="title" class="form-control" placeholder="Enter Last name">
               </div>
             <div class="form-group col-md-12">
                <label for="title"> Email </label>
                <input type="text" id="email" v-model="email" name="title" class="form-control" placeholder="Enter email">
            </div>
            <div class="form-group col-md-12">
                <label for="title"> Phone </label>
                <input type="text" id="phone_number" v-model="phone" name="title" class="form-control" placeholder="Enter Phone number">
            </div>
            <div class="form-group col-md-12">
                <label for="title"> Address </label>
                <input type="text" id="address" v-model="address" name="title" class="form-control" placeholder="Enter Address">
            </div>
              <div class="form-group col-md-12">
                  <label for="description"> Description </label>
                  <input type="text" id="description" v-model="description" name="description" class="form-control" placeholder="Enter Description">
              </div>
              <div class="form-group col-md-4 pull-right">
                  <button class="btn btn-success" type="submit"> Create Customer </button>
              </div>           </form>
        </div>
    </div>
</template>

This is the <template></template> section that contains the details of the input fields. Next, paste the following code just after the end of the </template> tag:

// ./src/components/customer/Create.vue

...

<script>
import axios from "axios";
import { server } from "../../helper";
import router from "../../router";
export default {
  data() {
    return {
      first_name: "",
      last_name: "",
      email: "",
      phone: "",
      address: "",
      description: ""
    };
  },
  methods: {
    createCustomer() {
      let customerData = {
        first_name: this.first_name,
        last_name: this.last_name,
        email: this.email,
        phone: this.phone,
        address: this.address,
        description: this.description
      };
      this.__submitToServer(customerData);
    },
    __submitToServer(data) {
      axios.post(`${server.baseURL}/customer/create`, data).then(data => {
        router.push({ name: "home" });
      });
    }
  }
};
</script>

Here, you created a method createCustomer() to receive the details of a customer via the input fields and used axios to post the data to the server.

Edit component

Similar to the CreateCustomer component, you need to create another component to edit customer’s details. Navigate to ./src/components/customer and create a new file named Edit.vue. Paste the following code in it:

// ./src/components/customer/Edit.vue

<template>
   <div>
        <h4 class="text-center mt-20">
         <small>
         <button class="btn btn-success" v-on:click="navigate()"> View All Customers </button>
         </small>
        </h4>
        <div class="col-md-12 form-wrapper">
          <h2> Edit Customer </h2>
          <form id="create-post-form" @submit.prevent="editCustomer">
               <div class="form-group col-md-12">
                <label for="title"> First Name </label>
                <input type="text" id="first_name" v-model="customer.first_name" name="title" class="form-control" placeholder="Enter firstname">
               </div>
               <div class="form-group col-md-12">
                <label for="title"> Last Name </label>
                <input type="text" id="last_name" v-model="customer.last_name" name="title" class="form-control" placeholder="Enter Last name">
               </div>
             <div class="form-group col-md-12">
                <label for="title"> Email </label>
                <input type="text" id="email" v-model="customer.email" name="title" class="form-control" placeholder="Enter email">
            </div>
            <div class="form-group col-md-12">
                <label for="title"> Phone </label>
                <input type="text" id="phone_number" v-model="customer.phone" name="title" class="form-control" placeholder="Enter Phone number">
            </div>
            <div class="form-group col-md-12">
                <label for="title"> Address </label>
                <input type="text" id="address" v-model="customer.address" name="title" class="form-control" placeholder="Enter Address">
            </div>
              <div class="form-group col-md-12">
                  <label for="description"> Description </label>
                  <input type="text" id="description" v-model="customer.description" name="description" class="form-control" placeholder="Enter Description">
              </div>
              <div class="form-group col-md-4 pull-right">
                  <button class="btn btn-success" type="submit"> Edit Customer </button>
              </div>           </form>
        </div>
    </div>
</template>
<script>
import { server } from "../../helper";
import axios from "axios";
import router from "../../router";
export default {
  data() {
    return {
      id: 0,
      customer: {}
    };
  },
  created() {
    this.id = this.$route.params.id;
    this.getCustomer();
  },
  methods: {
    editCustomer() {
      let customerData = {
        first_name: this.customer.first_name,
        last_name: this.customer.last_name,
        email: this.customer.email,
        phone: this.customer.phone,
        address: this.customer.address,
        description: this.customer.description
      };
      axios
        .put(
          `${server.baseURL}/customer/update?customerID=${this.id}`,
          customerData
        )
        .then(data => {
          router.push({ name: "home" });
        });
    },
    getCustomer() {
      axios
        .get(`${server.baseURL}/customer/customer/${this.id}`)
        .then(data => (this.customer = data.data));
    },
    navigate() {
      router.go(-1);
    }
  }
};
</script>

The route parameter was used here to fetch the details of a customer from the database and populated the inputs fields with it. As a user of the application, you can now edit the details and submit back to the server.

The editCustomer() method within the <script></script> was used to send a PUT HTTP request to the server.

View all customer

Finally, to fetch and show the complete list of customers from the server, you will create a new component. Navigate to the views folder within the src folder, you should see a Home.vue file, if otherwise, create it and paste this code in it:

// ./src/views/Home.vue
<template>
    <div class="container-fluid">
      <div class="text-center">
        <h1>Nest Customer List App Tutorial</h1>
       <p> Built with Nest.js, Vue.js and MongoDB</p>
       <div v-if="customers.length === 0">
            <h2> No customer found at the moment </h2>
        </div>
      </div>

        <div class="">
            <table class="table table-bordered">
              <thead class="thead-dark">
                <tr>
                  <th scope="col">Firstname</th>
                  <th scope="col">Lastname</th>
                  <th scope="col">Email</th>
                  <th scope="col">Phone</th>
                  <th scope="col">Address</th>
                  <th scope="col">Description</th>
                  <th scope="col">Actions</th>
                </tr>
              </thead>
              <tbody>
                <tr v-for="customer in customers" :key="customer._id">
                  <td>{{ customer.first_name }}</td>
                  <td>{{ customer.last_name }}</td>
                  <td>{{ customer.email }}</td>
                  <td>{{ customer.phone }}</td>
                  <td>{{ customer.address }}</td>
                  <td>{{ customer.description }}</td>
                  <td>
                    <div class="d-flex justify-content-between align-items-center">
                                <div class="btn-group" style="margin-bottom: 20px;">
                                  <router-link :to="{name: 'Edit', params: {id: customer._id}}" class="btn btn-sm btn-outline-secondary">Edit Customer </router-link>
                                  <button class="btn btn-sm btn-outline-secondary" v-on:click="deleteCustomer(customer._id)">Delete Customer</button>
                                </div>
                              </div>
                  </td>
                </tr>
              </tbody>
            </table>
          </div>
    </div>
</template>
<script>
import { server } from "../helper";
import axios from "axios";
export default {
  data() {
    return {
      customers: []
    };
  },
  created() {
    this.fetchCustomers();
  },
  methods: {
    fetchCustomers() {
      axios
        .get(`${server.baseURL}/customer/customers`)
        .then(data => (this.customers = data.data));
    },
    deleteCustomer(id) {
      axios
        .delete(`${server.baseURL}/customer/delete?customerID=${id}`)
        .then(data => {
          console.log(data);
          window.location.reload();
        });
    }
  }
};
</script>

Within <template> section, you created an HTML table to display all customers details and used the <router-link> to create a link for editing and to a view a single customer by passing the customer._id as a query parameter. And finally, within the <script> section of this file, you created a method named fetchCustomers() to fetch all customers from the database and updated the page with the data returned from the server.

Update App.vue

Open the AppComponent of the application and update it with the links to both Home and Create component by using the content below:

// ./src/App.vue

<template>
  <div id="app">
    <div id="nav">
      <router-link to="/">Home</router-link> |
      <router-link to="/create">Create</router-link>
    </div>
    <router-view/>
  </div>
</template>

<style>
...
.form-wrapper {
  width: 500px;
  margin: 0 auto;
}
</style>

Also included is a <style></style> section to include styling for the forms.

Include Bootstrap

Navigate to the index.html file within the public folder and include the CDN file for bootstrap as shown below. This is just to give the page some default style:

<!DOCTYPE html>
<html lang="en">
<head>
  ...
  <!-- Add this line -->
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css">
  <title>customer-list-app-frontend</title>
</head>
<body>
   ...
</body>
</html>

Setting up routing

Finally, configure the router file within ./src/router.js to include the link to all the required reusable components created so far by updating its content as shown here:

// ./src/router.js

import Vue from 'vue'
import Router from 'vue-router'
import HomeComponent from '@/views/Home';
import EditComponent from '@/components/customer/Edit';
import CreateComponent from '@/components/customer/Create';
Vue.use(Router)
export default new Router({
  mode: 'history',
  routes: [
    { path: '/', redirect: { name: 'home' } },
    { path: '/home', name: 'home', component: HomeComponent },
    { path: '/create', name: 'Create', component: CreateComponent },
    { path: '/edit/:id', name: 'Edit', component: EditComponent },
  ]
});

Test the application

You can now proceed to test the application by running npm run serve to start it and navigate to http://localhost:8080 to view it:

Ensure that the backend server is running at this moment, if otherwise, navigate to the backend application from a different terminal and run:

npm run start

Lastly, also ensure that the MongoDB instance is running as well. Use sudo mongod from another terminal on your local system, if it is not running at the moment.

Create new customer

Homepage ( View all customers )

Edit a customer details

Conclusion

In this tutorial, you have created a simple customer list management application by using Nest.js and Vue.js. Here, you used Nest.js to build a RESTful backend API and then leveraged on Vue.js to craft a client that consumes the API.

This has given you an overview of how to structure Nest.js application and integrate it with a MongoDB database.

I hope you found this tutorial helpful. Don’t hesitate to explore the source code of both application by checking it out here on GitHub.

Originally published by Olususi Kayode Oluyemi at https://scotch.io