Let’s Build a Web App with Vue, Chart.js and an API
Tags: vue, chartjs, npm, javascript
Published: 2017-04-19 17:05:20

---
title: Let’s Build a Web App with Vue, Chart.js and an API
published: true
description: The first part of building a small web app which interacts with the npm API and generates nice charts
cover_image: https://thepracticaldev.s3.amazonaws.com/i/38r0iqu3vim9vdte7yyi.png
tags: vuejs, chartjs, npm, javascript
---


Data is beautiful. And with modern technologies it is crazy easy to visualize your data and create great experiences. In this quick how to, we cover how to interact with the npm ðŸ’˜ API to get download statistics of a package and generate a chart from this data with Chart.js


# âš¡ Quickstart

We will build [npm-stats.org](http://npm-stats.org/#/) and will be using following tools:

- Vue.js with vue-router
- Chart.js
- vue-chartjs
- vue-cli
- axios

With Vue.js we will build the basic interface of the app and and routing with `vue-router`. And we scaffold our project with `vue-cli` which creates our basic project structure. For the chart generation we will be using Chart.js and as a wrapper for Vue, `vue-chartjs`. As we need to interact with an API, we’re using `axios` to make the http requests. However feel free to swap that one out with any other lib.

# ðŸ”§ Install & Setup

At first we need to install `vue-cli` to scaffold our project. I hope you have a current version of node and npm already installed! ðŸ™ Even better if you have yarn installed! If not, you really should! If you don’t want, just swap out the yarn commands with the npm equivalents.

```
$ npm install -g vue-cli
```

Then we can scaffold our project with vue-cli. If you want to can enable the unit and e2e tests, however we will not cover them.ðŸ”¥ But you need to check vue-router!

```bash
$ vue init webpack npm-stats
```

Then we cd in our project folder and install the dependencies with `cd npm-stats && yarn install`. So our basic project dependencies are installed. Now we need to add the one for our app.

```
$ yarn add vue-chartjs chart.js axios
```

Just a quick check if everything is running with `yarn run dev`. Now we should see the boilerplate page of vue.

Aaaand we’re done! ðŸ‘

# ðŸ’ª Time to build

Just a small disclaimer here, I will not focus on the styling. I guess you’re able to make the site look good by your own ðŸ’… so we only cover the javascript related code.
And another disclaimer, this is rather a small MVP then super clean code right now. I will refactor some of it in later stages. Like in the real world.

## Components

Let’s think about what components we need. As we’re looking at the screenshot we see an input field for the package name you’re looking for and a button. Maybe a header and footer and the chart itself.

You totally could make the button and input field a component however as we don’t build a complex app, why bother? Make it simple. Make it work!

So I ended up with following components:

- components/Footer.vue
- components/Header.vue
- components/LineChart.vue
- pages/Start.vue

I will skip the Header and Footer as they only contain the logo and some links. Nothing special here. The LineChart and Start page are the important ones.

## LineChart

The LineChart component will be our chart.js instance which renders the chart. We need to import the Line component and extend it. We create two props for now. One for the data which is the number of downloads and the labels which are for example the days, weeks, years.

```javascript
props: {
 chartData: {
   type: Array,
   required: false
 },
 chartLabels: {
   type: Array,
   required: true
 }
},
```

As we want all our charts to look the same, we define some of the Chart.js styling options in a data model which get passed as options to the **renderChart()** method.

And as we will have only one dataset for now, we can just build up the dataset array and bind the labels and data.

```html
<script>
  import { Line } from 'vue-chartjs'
  export default Line.extend({
    props: {
      chartData: {
        type: Array | Object,
        required: false
      },
      chartLabels: {
        type: Array,
        required: true
      }
    },
    data () {
      return {
        options: {
          scales: {
            yAxes: [{
              ticks: {
                beginAtZero: true
              },
              gridLines: {
                display: true
              }
            }],
            xAxes: [ {
              gridLines: {
                display: false
              }
            }]
          },
          legend: {
            display: false
          },
          responsive: true,
          maintainAspectRatio: false
        }
      }
    },
    mounted () {
      this.renderChart({
        labels: this.chartLabels,
        datasets: [
          {
            label: 'downloads',
            borderColor: '#249EBF',
            pointBackgroundColor: 'white',
            borderWidth: 1,
            pointBorderColor: '#249EBF',
            backgroundColor: 'transparent',
            data: this.chartData
          }
        ]
      }, this.options)
    }
  })
</script>
```

# ðŸ“º Our start page

As we have our LineChart component up and working. It’s time to build the rest. We need an input field and button to submit the package name. Then request the data and pass the data to our chart component.

So, let’s first think about what data we need and what states / data models. First of all we need a `package` data model, which we will use with v-model in our input field. We also want to display the name of the package as a headline. So `packageName` would be good. Then our two arrays for the requested data `downloads` and `labels` and as we’re requesting a time period we need to set the `period`. But, maybe the request goes wrong so we need `errorMessage` and `showError`. And last but not least `loaded` as we want to show the chart only after the request is made.

## npm API
There are various endpoints to get the downloads of a package. One is for example 

```
GET https://api.npmjs.org/downloads/point/{period}[/{package}]
```

However this one gets only a point value. So the total downloads. But to draw our cool chart, we need more data. So we need the range endpoint.

```
GET https://api.npmjs.org/downloads/range/{period}[/{package}]
```

The period can be defined as for example `last-day` or `last-month` or a specific date range `2017-01-01:2017-04-19` But to keep it simple we set the default value to `last-month`. Later in **Part II** we can then add some date input fields so the user can set a date range.

So our data models are looking like this:

```javascript
data () {
 return {
  package: null,
  packageName: â€˜’,
  period: â€˜last-month’,
  loaded: false,
  downloads: [],
  labels: [],
  showError: false,
  errorMessage: â€˜Please enter a package name’
 }
},
```

# ðŸ’… Template

Now it’s time to build up the template. We need 5 things:

- Input field
- Button to trigger the search
- Error message output
- Headline with the package name
- Our chart.

```
<input
 class=”Search__input”
 @keyup.enter=”requestData”
 placeholder=”npm package name”
 type=”search” name=”search”
 v-model=”package”
 >
<button class=”Search__button” @click=”requestData”>Find</button>
<div class="error-message" v-if="showError">
  {{ errorMessage }}
</div>
<h1 class="title" v-if="loaded">{{ packageName }}</h1>
<line-chart v-if="loaded" :chart-data="downloads" :chart-labels="labels"></line-chart>
```

Just ignore the css classes for now. We have our Input Field which has an keyup event on enter. So if you press enter you trigger the `requestData()` method. And we bind `v-model` to package

For the potential error we have a condition, only if `showError` is true we show the message. There are two types or errors that can occur. The one is, someone try to search for a package without entering any name or he’s entering a name that does not exist.

For the first case, we have our default errorMessage, for the second case we will grab the error message that comes from the request.

So our full template will look like this:

```html
<template>
  <div class="content">
    <div class="container">
      <div class="Search__container">
        <input
          class="Search__input"
          @keyup.enter="requestData"
          placeholder="npm package name"
          type="search" name="search"
          v-model="package"
        >
        <button class="Search__button" @click="requestData">Find</button>
      </div>
      <div class="error-message" v-if="showError">
       {{ errorMessage }}
      </div>
      <hr>
      <h1 class="title" v-if="loaded">{{ packageName }}</h1>
      <div class="Chart__container" v-if="loaded">
        <div class="Chart__title">
          Downloads per Day <span>{{ period }}</span>
          <hr>
        </div>
        <div class="Chart__content">
          <line-chart v-if="loaded" :chart-data="downloads" :chart-labels="labels"></line-chart>
        </div>
      </div>
    </div>
  </div>
</template>
```

# ðŸ¤– Javascript

Now it’s time for the coding. First we will do our requestData() method. It is rather simple. We need to make a request to our endpoint and then map the data that comes in. In our response.data we have some information about the package:

![response.data](https://i.imgur.com/r0WjLCi.png)

Like the start data, end date, the package name and then the downloads array. However the structure for the downloads array is something like this:

```javascript
downloads: [
 {day: â€˜2017–03–20’, downloads: â€˜3’},
 {day: â€˜2017–03–21’, downloads: â€˜2’},
 {day: â€˜2017–03–22’, downloads: â€˜10’},
]
```

But we need to separate the downloads and days, because for chart.js we need one array only with the data (downloads) and one array with the labels (day). This is an easy job for map.

```javascript
requestData () {
 axios.get(`https://api.npmjs.org/downloads/range/${this.period}/${this.package}`)
 .then(response => {
   this.downloads = response.data.downloads.map(download => download.downloads)
   this.labels = response.data.downloads.map(download => download.day)
   this.packageName = response.data.package
   this.loaded = true
 })
 .catch(err => {
   this.errorMessage = err.response.data.error
   this.showError = true
 })
}
```

Now if we enter a package name, like vue and hit enter, the request is made, the data mapped and the chart rendered! But, wait. You don’t see anything. Because we need to tell vue-router to set the index to our start page.

Under `router/index.js` we import or page and tell the router to use it

```javascript
import Vue from â€˜vue’
import Router from â€˜vue-router’
import StartPage from â€˜@/pages/Start’
Vue.use(Router)
export default new Router({
 routes: [
   {
     path: â€˜/’,
     name: â€˜Start’,
     component: StartPage
   },
 ]
})
```

# ðŸ’Ž Polish

But, we are not done yet. We have some issues, right? First our app breaks if we don’t enter any name. And we have problems if you enter a new package and hit enter. And after an error the message does not disappear.

Well, it’s time to clean up a bit. First let’s create a new method to reset our state.

```javascript
resetState () {
 this.loaded = false
 this.showError = false
},
```

Which we call in our `requestData()` method before the `axios` api call. And we need a check for the package name.

```javascript
if (this.package === null 
    || this.package === â€˜’ 
    || this.package === â€˜undefined’) {
  this.showError = true
  return
}
```

Now if we try to search an empty package name, we get or default errorMessage.


I know, we covered a lot, but let’s add another small cool feature. We have `vue-router`, but not really using it. At our root `/` we see the starting page with the input field. And after a search we stay at our root page. But it would be cool if we could share our link with the stats, wouldn’t it be?

So after a valid search, we add the package name to our url.

`npm-stats.org/#/vue-chartjs`

And if we click on that link, we need to grab the package name and use it to request our data.
Let’s create a new method to set our url

```javascript
setURL () {
 history.pushState({ info: `npm-stats ${this.package}`}, this.package, `/#/${this.package}`)
 }
```

We need to call `this.setURL()` in our response promise. Now after the request is made, we add the package name to our URL. But, if we open a new browser tab and call it, nothing happens. Because we need to tell `vue-router` that everything after our `/` will also point to the start page and define the string as a query param. Which is super easy.

In our `router/index.js` we just need to set another path in the routes array. We call the param package.

```javascript
{
  path: â€˜/:package’,
  component: StartPage
}
```

Now if you go to `localhost:8080/#/react-vr` you will get the start page. But without a chart. Because we need to grab the param and do our request with it.

Back in our `Start.vue` we grab the param in the mounted hook.

```javascript
mounted () {
 if (this.$route.params.package) {
   this.package = this.$route.params.package
   this.requestData()
 }
},

```

And thats it! Complete file:

```javascript
 import axios from 'axios'
  import LineChart from '@/components/LineChart'
  export default {
    components: {
      LineChart
    },
    props: {},
    data () {
      return {
        package: null,
        packageName: '',
        period: 'last-month',
        loaded: false,
        downloads: [],
        labels: [],
        showError: false,
        errorMessage: 'Please enter a package name'
      }
    },
    mounted () {
      if (this.$route.params.package) {
        this.package = this.$route.params.package
        this.requestData()
      }
    },
    methods: {
      resetState () {
        this.loaded = false
        this.showError = false
      },
      requestData () {
        if (this.package === null || this.package === '' || this.package === 'undefined') {
          this.showError = true
          return
        }
        this.resetState()
        axios.get(`https://api.npmjs.org/downloads/range/${this.period}/${this.package}`)
          .then(response => {
            console.log(response.data)
            this.downloads = response.data.downloads.map(download => download.downloads)
            this.labels = response.data.downloads.map(download => download.day)
            this.packageName = response.data.package
            this.setURL()
            this.loaded = true
          })
          .catch(err => {
            this.errorMessage = err.response.data.error
            this.showError = true
          })
      },
      setURL () {
        history.pushState({ info: `npm-stats ${this.package}` }, this.package, `/#/${this.package}`)
      }
    }
  }
```


You can check out the full source at [GitHub](https://github.com/apertureless/npm-stats) and view the demo page at ðŸ“º [npm-stats.org](http://npm-stats.org/)

# Improvements

But hey, there is still room for improvements. We could add more charts. Like monthly statistics, yearly statistics and add date fields to set the period and many more things. I will cover some of them in **Part II** ! So stay tuned!