From npm init to a Single-Page Application in Vue.js, Part 3

In my last post in this series I added some global state via a vuex store and got the conditional admin settings set up. In this post we’re going to add a simple Express.js server to store our todos and admin options. Express.js is a lightweight web framework that supports a wide variety of plugins, and is an ideal candidate for making a quick and dirty REST API to deliver our data back and forth from client to server. It would also be useful to move away from the webpack dev server so that we can use a setup like this in a production environment. To do that, we’ll need to:

  1. Create an express server with API routes.
  2. Serve our static files compiled by webpack with Express
  3. Edit our NPM scripts to allow for a good development workflow

We’re also going to be doing some housekeeping around our package manager. Yarn is a faster alternative to NPM that caches packages locally so we don’t have to download common packages more than once. This makes Yarn faster to install dependencies than NPM. Thankfully, Yarn can read a normal NPM generated package.json file and install all the dependencies necessary for our app with no changes to the application. I’m on windows so installing yarn is as easy as installing chocolatey package manager and typing cinst yarn -y to install Yarn itself. Then I just change the directory to my project and type yarn install . This will create a yarn.lock file instead of an NPM generated package.lock file. After that we can just delete the NPM lock file, or keep it if your team uses both NPM and Yarn.

Now onto our Express server. Here’s our app.js file:

var express = require('express');
    bodyParser = require("body-parser");
    path = require('path');
        router = require('./src/controllers/api.controller');
    app = express(),
    PORT_NUMBER = 3000,
    

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));

app.use('/dist', express.static('dist'));

app.use('/api', router);

app.get('*', function(req, res) {
    res.sendFile(path.resolve('index.html'));
});

app.listen(PORT_NUMBER);

I originally had all the routes for our express server in the same app.js file, but I broke it out into it’s own separate “controller” so that I could make changes to the “/api” route in a single file. Here’s what that controller looks like:

var express = require('express'),
    router = express.Router(),
    todos = [ 
        { description: "Example todo!", done: false },
            { description: "Another Example todo!", done: true }
        ],
    adminOptions = {
            zebra: { description: "Zebra stripes", checked: true },
            strikeThrough: { description: "Strikethrough items", checked: false },
            saltyDeveloper: { description: "Salty Developer Mode", checked: false }
        },
    saltyPhrases = [
            'Consider ASP.NET career',
            'Vomit cause I considered an ASP.NET career (or was it the sushi?)',
            'Remove \'I dreamed a dream\' (new version) from coding playlist',
            'Cry because I don\'t actually know how to build anything useful',
            'Pretend that ruby on rails performance is good enough for production',
            'Learn erlang + elixir (later)',
            'Get those TPS to boss',
            'Resent getting TPS to boss',
            'Play with my whitespace configuration (again)',
            'Create another project repo that I\'m never going to keep updated',
            'Add \'Sound of Silence\' to coding playlist',
            'Consider how awesome Jinja 2 templating is',
            'Wish you were one of the cool kids that does web dev in Go or Python',
            'Wish you were one of the coolest kids that does systems programming',
            'Realize that systems programming is super hard',
            'Thank your lucky stars that someone figured out network interfaces before you were born'
        ];

router.route('/todos')
    .get(function(req, res) {
        res.send(todos);
    })
    .post(function(req, res) {
        todos.push(req.body);
        res.send(todos);
    });

router.route('/todos/:todo')
    .get(function(req, res) {
        res.send(todos[req.params.todo - 1]);
    })  
    .put(function(req, res) {
        todos[req.params.todo] = req.body;
        res.send(todos);
    })
    .delete(function(req, res) {
        var removedItem = todos.splice((req.params.todo - 1), 1);
        res.send(todos);
    });

router.route('/adminOptions/:option')
    .put(function(req, res) {
        adminOptions[req.params.option] = req.body;
        res.send(adminOptions);
    });

router.route('/adminOptions')
    .get(function(req, res) {
        res.send(adminOptions);
    });

router.route('/saltyPhrases')
    .get(function(req, res) {
        res.send(saltyPhrases);
    })

router.route('/dataReady')
    .get(function(req, res) {
        res.send(true);
    })

module.exports = router;

Most of these routes just send the data that’s already in memory back to the client. In a future post, I’m going to remove the in memory data structures from this file and move them to MongoDB for persistence. For now we’ll just leave the data in memory, but we need some way to get this data to the client application. To do this, we’ll use our vuex store we created in part 2 in conjunction with a plugin for rest apis called vuex-rest-api. Go ahead and run yarn add vuex-rest-api axios -S to install the plugin we need and one of its dependencies, axios. Axios is a promise based http request module to make writing AJAX requests a little easier. I install it explicitly so that I can import and create an instance for use in our vuex-rest-api plugin. Now, in our store.js file we’ll add an import and start creating methods to get data:

import Vue from 'vue'
import Vuex from 'vuex'
import Vapi from 'vuex-rest-api'
import axios from 'axios'
import * as getters from './getters'

Vue.use(Vuex);

var ax = axios.create({
    timeout: 1000,
    headers: {'Content-Type': 'application/json'}
});

var todos = new Vapi({
    axios: ax,
    baseURL: "http://localhost:3000",
    state: {
        todo: null,
        todos: [],
        saltyPhrases: [],
        adminOptions: {},
        dataReady: false
    }
})
.get({
    action: "getTodos",
    property: "todos",
    path: "/api/todos"
})
.post({
    action: "addTodo",
    property: "todos",
    path: "/api/todos"
})
.put({
    action: "setTodo",
    property: "todos",
    path: ({index}) => "/api/todos/" + index
})
.get({
    action: "getAdminOptions",
    property: "adminOptions",
    path: "/api/adminOptions"    
})
.put({
    action: "setAdminOptions",
    property: "adminOptions",
    path: ({ index }) => '/api/adminOptions/' + index
})
.get({
    action: "getSaltyPhrases",
    property: "saltyPhrases",
    path: "/api/saltyPhrases",    
})
.get({
    action: "setDataReady",
    property: "dataReady",
    path: "/api/dataReady" 
})
.getStore();

todos.getters = getters;


export const store = new Vuex.Store(todos);

In the code above, we create a ‘Vapi’ instance and append different http method functions to it to get the functionality we want. If we want to get data into our vuex store, all we have to do is invoke this.$store.dispatch(‘functionName’); in any vue component that has our store loaded. See the code below.

import { mapGetters } from 'vuex';

export default {
    name: 'home',
    data() {
        return {
            message: "Todo App",
            tempMessage: ''
        };
    },
    computed: {
        ...mapGetters([
            'getTodos',
            'getSaltyPhrases',
            'isSaltyDeveloperMode',
            'isZebraStripesMode',
            'isStrikeThroughMode',
            'isDataReady'
        ])
    },
    methods: {
        addTodo() {
            var tempObject = { "description": '', "checked": false };
            if (this.$store.getters.isSaltyDeveloperMode) {
                var saltyPhrases = this.$store.getters.getSaltyPhrases;
                var saltyIndex = Math.floor(Math.random() * saltyPhrases.length);
                tempObject.description = saltyPhrases[saltyIndex];
            }
            else {
                tempObject.description = this.tempMessage;
            }

            this.$store.dispatch("addTodo", {data: tempObject});
            this.tempMessage = '';
        },
        updateTodo(event) {
            var obj = this.$store.getters["getTodos"][event.target.id];
            obj.done = !obj.done;
            this.$store.dispatch('setTodo', {params: { index: event.target.id }, data: obj });
        }
    },
    mounted() {
        this.$store.dispatch('getTodos');
        this.$store.dispatch('getAdminOptions');
        this.$store.dispatch('getSaltyPhrases');
        this.$store.dispatch('setDataReady');
    }
}

In the code above we call the dispatch functions to get our vuex store data when the component is mounted. This allows us to avoid getting unhandled exceptions in the rendering function for the todos. The next post will be about MongoDB and setting up persistence with mongoose ORM.