Build a task app with Hapi, MongoDB and Vue.js

Hapi Node.js framework for the server, MongoDB for the database and Vue.js for the client side application.

The idea for this tutorial is we’re going to build a task app with Node.js, MongoDB and Vue.js. Users will be able to read, create and delete tasks from their web browser. All of the tasks will be stored in the database and updates will happen asynchronously from the web browser. That means users won’t have to refresh the page in order to see the changes take effect. This application is straightforward and should provide a clear introduction to how build with Node.js, MongoDB and Vue.js.

The final product will look something like this:

Example todo app using Node.js server, Mongo database and Vue client side app.

🐙 The server side code is open source on GitHub.

🐱 The client side code is open source on GitHub.

To begin with, we’re going to start by building the server. Therefore we’ll have to introduce…

Hapijs

Hapi is a Node.js server framework that’s used by great companies such as Lob, Stitch Labs and Auth0. To get started generate a project with npm init and use this as your main server file:

const Hapi     = require('hapi');
const routes = require('./routes');
require('./utils/database');
const server = Hapi.server({
port: 3000,
host: 'localhost',
routes: { cors: true }
});
const startServer = async () => {
try {
routes.forEach((route)=>{
server.route(route);
});
await server.start();
console.log(`Server running at: ${server.info.uri}`);
} catch (err) {
console.error(err);
}
};
startServer();
module.exports = server;

If you’re familiar with express this looks pretty similar to an express app. We can see clearly though we’re using async / await functions. This is a new feature of recent Node.js version and a great addition to the Javascript language. Essentially the function, prefixed with “async” can hold execution until the await promise call is returned. Async functions return promises.

We’re configuring cors here with a default Hapi option and connecting to the database through a require statement.

MongoDB and Mongoose

To connect an d query MongoDB we’re going to use an ORM called Mongoose that’s for querying and writing to Mongo.

const mongoose = require('mongoose');
require('../models');
mongoose.connect('mongodb://localhost/task-app-backend', {
useNewUrlParser: true
}, (err) => {
if (err) throw err;
});

That connects to the MongoDB database (you might need to have mongod running in a separate tab on your local machine).

With MongoDB there are no database migrations. The ORM wrapper has a concept of models that we can take advantage. Since this is a task app we’ll create a Task model.

const mongoose = require('mongoose');
const taskModel = mongoose.Schema({
name: {type: String, required: '{PATH} is required!'},
description: {type: String},
}, {
timestamps: true
});
module.exports = mongoose.model('Task', taskModel);

This sets up a MongoDB collection for us, which is basically a table shaped like a big old Javascript object. They use something called BSON that they wrote a white paper about at MongoDB.

MongoDB stock price.

In contrast, Mongoose is an open source npm package.


We can define our routes as a Javascript array of objects that each have “method”, “path” and “handler” properties. There’s an optional “options” property you can also include that we’ll tie to the api/index.js file.

Hapi routing docs
const api = require('./api');
const routes = [
{
method: 'GET',
path: '/',
handler: (request, h) => {
return {success: true};
}
},
{
method: 'GET',
path: '/api',
handler: (request, h) => {
return {success: true};
}
},
{
method: 'GET',
path: '/api/task',
options: api.task.all
},
{
method: 'POST',
path: '/api/task',
options: api.task.create
},
{
method: 'GET',
path: '/api/task/{task}',
options: api.task.get
},
{
method: 'PUT',
path: '/api/task/{task}',
options: api.task.update
},
{
method: 'DELETE',
path: '/api/task/{task}',
options: api.task.remove
},
];
module.exports = routes;

Finally for the CRUD endpoints this is what I have:

const {Task} = require('./../models');
const Boom = require('boom');
const taskApi = {
all: {
async handler(request, h) {
try {
return await Task.find({}).sort({ createdAt: 'desc' });
      } catch (err) {
Boom.badImplementation(err);
}
}
},
create: {
async handler(request, h) {
try {
const task = await new Task({
name: request.payload.name,
description: request.payload.description
});
task.save();
        return { message: "Task created successfully", task };
      } catch (err) {
Boom.badImplementation(err);
}
}
},
get: {
async handler(request, h) {
try {
const task = request.params.task;
        return await Task.findOne({
_id: task.id
});
      } catch (err) {
Boom.badImplementation(err);
}
}
},
update: {
async handler(request, h) {
try {
const task = request.params.task;
const updates = request.payload;
        // todo: submit a pull request
      } catch (err) {
Boom.badImplementation(err);
}
}
},
remove: {
async handler(request, h){
try {
const task = await Task.findById(request.params.task).remove();
            return { success: true, message: 'Successfully removed task!' };
        } catch (err) {
Boom.badImplementation(err);
}
}
}
};
module.exports = taskApi;

This file uses Mongoose to fetch our records from the database. The await calls resolve promises and block execution while the promise resolves. The request object comes from our Vue.js application, which will be housed in a separate repo.

Vue.js

This is a framework comparison of Vue, React, Lindsay Lohan and Taylor Swift.

It looks like Google is predicting big things from Taylor in the coming weeks.

Taylor swift is clear winner of the framework comparison from the graph above. Sorry React.

Anyways, the front end source code is here. It uses a handy app generator called vue-webpack-simple that’s maintained by the Vue.js core team.

One funky bit I learned about while building this is there’s a transition and transition-group component that you can use to queue up animations with CSS. The HTML for the component be like:

<transition-group name="task-list">    
<div class="row mb-2" v-for="(task, index) in tasks" :key="task._id">
<div class="col-sm-4">
{{ task.name }}
</div>
<div class="col-sm-2">
<span @click='updateTask(task._id, index)' class="task-action"><i class="fas fa-pencil-alt"></i>
</span>
<span @click='deleteTask(task._id, index)' class="task-action badge badge-danger badge-pill">X</span>
</div>
</div>
</transition-group>

There’s great starter sample code for a lot of different use cases on the Vue.js homepage for documentation: Enter/Leave & List Transitions.

Vue.js applies special classes when enter and leave transitions happen. By giving the TransitionGroup a name of “task-list” task-list gets appended to the special Vue.js event class names:

.task-list-item {
display: inline-block;
margin-right: 10px;
}
.task-list-enter-active, .task-list-leave-active {
transition: opacity .5s;
}
.task-list-enter, .task-list-leave-to {
opacity: 0;
}

The Vue.js events in conjunction with the transition CSS property are responsible for the fades when we add and delete tasks. (Video at the top.)

That’s about it! The open source code links are below:

🐙 The server side code is open source on GitHub.

🐱 The client side code is open source on GitHub.


If you’re on the job market in the Bay Area please consider creating a Job Seeker profile on Employbl.com.