JavaScript
Article
By James Kolce

How to Build and Structure a Node.js MVC Application

By James Kolce

Inside the monitor a puppet manipulates on-screen windows and popups, controlling a Node.js MVC application.

In a non-trivial application, the architecture is as important as the quality of the code itself. We can have well-written pieces of code, but if we don’t have a good organization, we will have a hard time as the complexity increases. There is no need to wait until the project is half-way done to start thinking about the architecture; the best time is before starting, using our goals as beacons for our choices.

Node.js doesn’t have a de facto framework with strong opinions on architecture and code organization in the same way that Ruby has the Rails framework, for example. As such, it can be difficult to get started with building full web applications with Node.

In this article, we are going to build the basic functionality of a note-taking app using the MVC architecture. To accomplish this we are going to employ the Hapi.js framework for Node.js and SQLite as a database, using Sequelize.js, plus other small utilities to speed up our development. We are going to build the views using Pug, the templating language.

What is MVC?

Model-View-Controller (or MVC) is probably one of the most popular architectures for applications. As with a lot of other cool things in computer history, the MVC model was conceived at PARC for the Smalltalk language as a solution to the problem of organizing applications with graphical user interfaces. It was created for desktop applications, but since then, the idea has been adapted to other mediums including the web.

We can describe the MVC architecture in simple words:

Model: The part of our application that will deal with the database or any data-related functionality.

View: Everything the user will see. Basically the pages that we are going to send to the client.

Controller: The logic of our site, and the glue between models and views. Here we call our models to get the data, then we put that data on our views to be sent to the users.

Our application will allow us to publish, see, edit and delete plain-text notes. It won’t have other functionality, but because we will have a solid architecture already defined we won’t have big trouble adding things later.

You can check out the final application in the accompanying GitHub repository, so you get a general overview of the application structure.

Laying out the Foundation

The first step when building any Node.js application is to create a package.json file, which is going to contain all of our dependencies and scripts. Instead of creating this file manually, NPM can do the job for us using the init command:

npm init -y

After the process is complete will get a package.json file ready to use.

Note: If you’re not familiar with these commands, checkout our Beginner’s Guide to npm.

We are going to proceed to install Hapi.js—the framework of choice for this tutorial. It provides a good balance between simplicity, stability and feature availability that will work well for our use case (although there are other options that would also work just fine).

npm install --save hapi hoek

This command will download the latest version of Hapi.js and add it to our package.json file as a dependency. It will also download the Hoek utility library that will help us write shorter error handlers, among other things.

Now we can create our entry file; the web server that will start everything. Go ahead and create a server.js file in your application directory and all the following code to it:

'use strict';

const Hapi = require('hapi');
const Hoek = require('hoek');
const Settings = require('./settings');

const server = new Hapi.Server();
server.connection({ port: Settings.port });

server.route({
  method: 'GET',
  path: '/',
  handler: (request, reply) => {
    reply('Hello, world!');
  }
});

server.start((err) => {
  Hoek.assert(!err, err);

  console.log(`Server running at: ${server.info.uri}`);
});

This is going to be the foundation of our application.

First, we indicate that we are going to use strict mode, which is a common practice when using the Hapi.js framework.

Next, we include our dependencies and instantiate a new server object where we set the connection port to 3000 (the port can be any number above 1023 and below 65535.)

Our first route for our server will work as a test to see if everything is working, so a ‘Hello, world!’ message is enough for us. In each route, we have to define the HTTP method and path (URL) that it will respond to, and a handler, which is a function that will process the HTTP request. The handler function can take two arguments: request and reply. The first one contains information about the HTTP call, and the second will provide us with methods to handle our response to that call.

Finally, we start our server with the server.start method. As you can see, we can use Hoek to improve our error handling, making it shorter. This is completely optional, so feel free to omit it in your code, just be sure to handle any errors.

Storing Our Settings

It is good practice to store our configuration variables in a dedicated file. This file exports a JSON object containing our data, where each key is assigned from an environment variable—but without forgetting a fallback value.

In this file, we can also have different settings depending on our environment (e.g. development or production). For example, we can have an in-memory instance of SQLite for development purposes, but a real SQLite database file on production.

Selecting the settings depending on the current environment is quite simple. Since we also have an env variable in our file which will contain either development or production, we can do something like the following to get the database settings (for example):

const dbSettings = Settings[Settings.env].db;

So dbSettings will contain the setting of an in-memory database when the env variable is development, or will contain the path of a database file when the env variable is production.

Also, we can add support for a .env file, where we can store our environment variables locally for development purposes; this is accomplished using a package like dotenv for Node.js, which will read a .env file from the root of our project and automatically add the found values to the environment. You can find an example in the dotenv repository.

Note: If you decide to also use a .env file, make sure you install the package with npm install -s dotenv and add it to .gitignore so you don’t publish any sensitive information.

Our settings.js file will look like this:

// This will load our .env file and add the values to process.env,
// IMPORTANT: Omit this line if you don't want to use this functionality
require('dotenv').config({silent: true});

module.exports = {
  port: process.env.PORT || 3000,
  env: process.env.ENV || 'development',

  // Environment-dependent settings
  development: {
    db: {
      dialect: 'sqlite',
      storage: ':memory:'
    }
  },
  production: {
    db: {
      dialect: 'sqlite',
      storage: 'db/database.sqlite'
    }
  }
};

Now we can start our application by executing the following command and navigating to localhost:3000 in our web browser.

node server.js

Note: This project was tested on Node v6. If you get any errors, ensure you have an updated installation.

Defining the Routes

The definition of routes gives us an overview of the functionality supported by our application. To create our additional routes, we just have to replicate the structure of the route that we already have in our server.js file, changing the content of each one.

Let’s start by creating a new directory called lib in our project. Here we are going to include all the JS components. Inside lib, let’s create a routes.js file and add the following content:

'use strict';

module.exports = [
  // We are going to define our routes here
];

In this file, we will export an array of objects that contain each route of our application. To define the first route, add the following object to the array:

{
  method: 'GET',
  path: '/',
  handler: (request, reply) => {
    reply('All the notes will appear here');
  },
  config: {
    description: 'Gets all the notes available'
  }
},

Our first route is for the home page (/) and since it will only return information we assign it a GET method. For now, it will only give us the message All the notes will appear here, which we are going to change later for a controller function. The description field in the config section is only for documentation purposes.

Then, we create the four routes for our notes under the /note/ path. Since we are building a CRUD application, we will need one route for each action with the corresponding HTTP method.

Add the following definitions next to the previous route:

{
  method: 'POST',
  path: '/note',
  handler: (request, reply) => {
    reply('New note');
  },
  config: {
    description: 'Adds a new note'
  }
},
{
  method: 'GET',
  path: '/note/{slug}',
  handler: (request, reply) => {
    reply('This is a note');
  },
  config: {
    description: 'Gets the content of a note'
  }
},
{
  method: 'PUT',
  path: '/note/{slug}',
  handler: (request, reply) => {
    reply('Edit a note');
  },
  config: {
    description: 'Updates the selected note'
  }
},
{
  method: 'GET',
  path: '/note/{slug}/delete',
  handler: (request, reply) => {
    reply('This note no longer exists');
  },
  config: {
    description: 'Deletes the selected note'
  }
},

We have done the same as in the previous route definition, but this time we have changed the method to match the action we want to execute.

The only exception is the delete route. In this case, we are going to define it with the GET method rather than DELETE and add an extra /delete in the path. This way, we can call the delete action just by visiting the corresponding URL.

Note: If you plan to implement a strict REST interface, then you would have to use the DELETE method and remove the /delete part of the path.

We can name parameters in the path by surrounding the word in brackets ({}). Since we are going to identify notes by a slug, we add {slug} to each path, with the exception of the PUT route; we don’t need it there because we are not going to interact with a specific note, but to create one.

You can read more about Hapi.js routes on the official documentation.

Now, we have to add our new routes to the server.js file. Let’s import the routes file at the top of the file:

const Routes = require('./lib/routes');

and replace our current test route with the following:

server.route(Routes);
--ADVERTISEMENT--

Building the Models

Models allow us to define the structure of the data and all the functions to work with it.

In this example, we are going to use the SQLite database with Sequelize.js which is going to provide us with a better interface using the ORM (Object-Relational Mapping) technique. It will also provide us a database-independent interface.

Setting up the database

For this section we are going to use Sequelize.js and SQlite. You can install and include them as dependencies by executing the following command:

npm install -s sequelize sqlite3

Now create a models directory inside lib/ with a file called index.js which is going to contain the database and Sequelize.js setup, and include the following content:

'use strict';

const Fs = require('fs');
const Path = require('path');
const Sequelize = require('sequelize');
const Settings = require('../../settings');

// Database settings for the current environment
const dbSettings = Settings[Settings.env].db;

const sequelize = new Sequelize(dbSettings.database, dbSettings.user, dbSettings.password, dbSettings);
const db = {};

// Read all the files in this directory and import them as models
Fs.readdirSync(__dirname)
  .filter((file) => (file.indexOf('.') !== 0) && (file !== 'index.js'))
  .forEach((file) => {
    const model = sequelize.import(Path.join(__dirname, file));
    db[model.name] = model;
  });

db.sequelize = sequelize;
db.Sequelize = Sequelize;

module.exports = db;

First, we include the modules that we are going to use:

  • Fs, to read the files inside the models folder, which is going to contain all the models.
  • Path, to join the path of each file in the current directory.
  • Sequelize, that will allow us to create a new Sequelize instance.
  • Settings, which contains the data of our settings.js file from the root of our project.

Next, we create a new sequelize variable that will contain a Sequelize instance with our database settings for the current environment. We are going to use sequelize to import all the models and make them available in our db object.

The db object is going to be exported and will contain our database methods for each model; it will be available in our application when we need to do something with our data.

To load all the models, instead of defining them manually, we look for all the files inside the models directory (with the exception of the index.jsfile) and load them using the import function. The returned object will provide us with the CRUD methods, which we then add to the db object.

At the end, we add sequelize and Sequelize as part of our db object, the first one is going to be used in our server.js file to connect to the database before starting the server, and the second one is included for convenience if you need it in other files too.

Creating our Note model

In this section we are going to use the Moment.js package to help with Date formatting. You can install it and include it as a dependency with the following command:

npm install -s moment

Now, we are going to create a note.js file inside the models directory, which is going to be the only model in our application; it will provide us with all the functionality we need.

Add the following content to that file:

'use strict';

const Moment = require('moment');

module.exports = (sequelize, DataTypes) => {
  let Note = sequelize.define('Note', {
    date: {
      type: DataTypes.DATE,
      get: function () {
        return Moment(this.getDataValue('date')).format('MMMM Do, YYYY');
      }
    },
    title: DataTypes.STRING,
    slug: DataTypes.STRING,
    description: DataTypes.STRING,
    content: DataTypes.STRING
  });

  return Note;
};

We export a function that accepts a sequelize instance, to define the model, and a DataTypes object with all the types available in our database.

Next, we define the structure of our data using an object where each key corresponds to a database column and the value of the key defines the type of data that we are going to store. You can see the list of data types in the Sequelize.js documentation. The tables in the database are going to be created automatically based on this information.

In the case of the date column, we also define a how Sequelize should return the value using a getter function (get key). We indicate that before returning the information it should be first passed through the Moment utility to be formatted in a more readable way (MMMM Do, YYYY).

Note: Although we are getting a simple and easy to read date string, it is stored as a precise date string product of the Date object of JavaScript. So this is not a destructive operation.

Finally, we return our model.

Synchronizing the Database

Now, we have to synchronize our database before we are able to use it in our application. In server.js, import the models at the top of the file:

// Import the index.js file inside the models directory
const Models = require('./lib/models/');

Next, replace the following code block:

server.start((err) => {
  Hoek.assert(!err, err);
  console.log(`Server running at: ${server.info.uri}`);
});

with this one:

Models.sequelize.sync().then(() => {
  server.start((err) => {
    Hoek.assert(!err, err);
    console.log(`Server running at: ${server.info.uri}`);
  });
});

This code is going to synchronize the models to our database and, once that is done, the server will be started.

Building the controllers

Controllers are functions that accept the request and reply objects from Hapi.js. The request object contains information about the requested resource and we use reply to return information to the client.

In our application, we are going to return only a JSON object for now, but we will add the views once we build them.

We can think of controllers as functions that will join our models with our views; they will communicate with our models to get the data, and then return that data inside a view.

The Home Controller

The first controller that we are going to build will handle the home page of our site. Create a home.js file inside the lib/controllers directory with the following content:

'use strict';

const Models = require('../models/');

module.exports = (request, reply) => {
  Models.Note
    .findAll({
      order: [['date', 'DESC']]
    })
    .then((result) => {
      reply({
        data: {
          notes: result
        },
        page: 'Home—Notes Board',
        description: 'Welcome to my Notes Board'
      });
    });
};

First, we get all the notes in our database using the findAll method of our model. This function will return a promise and, if it resolves, we will get an array containing all the notes in our database.

We can arrange the results in descending order, using the order parameter in the options object passed to the findAll method, so the last item will appear first. You can check all the available options in the Sequelize.js documentation.

Once we have the home controller, we can edit our routes.js file. First, we import the module at the top of the file, next to the Path module import:

const Home = require('./controllers/home');

Then we add the controller we just made to the array:

{
  method: 'GET',
  path: '/',
  handler: Home,
  config: {
    description: 'Gets all the notes available'
  }
},

Boilerplate of the Note Controller

Since we are going to identify our notes with a slug, we can generate one using the title of the note and the slug library, so let’s install it and include it as a dependency with the following command:

npm install -s slug

The last controller that we have to define in our application will allow us to create, read, update, and delete notes.

We can proceed to create a note.js file inside the lib/controllers directory and add the following content:

'use strict';

const Models = require('../models/');
const Slugify = require('slug');
const Path = require('path');

module.exports = {
  // Here we are going to include our functions that will handle each request in the routes.js file.
};

The “create” function

To add a note to our database, we are going to write a create function that is going to wrap the create method on our model using the data contained in the payload object.

Add the following inside the object that we are exporting:

create: (request, reply) => {
  Models.Note
    .create({
      date: new Date(),
      title: request.payload.noteTitle,
      slug: Slugify(request.payload.noteTitle, {lower: true}),
      description: request.payload.noteDescription,
      content: request.payload.noteContent
    })
    .then((result) => {
      // We are going to generate a view later, but for now lets just return the result.
      reply(result);
    });
},

Once the note is created, we will get back the note data and send it to the client as JSON using the reply function.

For now, we just return the result, but once we build the views in the next section, we will be able to generate the HTML with the new note and add it dynamically on the client. Although this is not completely necessary and will depend on how you are going to handle your front-end logic, we are going to return an HTML block to simplify the logic on the client.

Also, note that the date is being generated on the fly when we execute the function, using new Date().

The “read” function

To search just one element we use the findOne method on our model. Since we identify notes by their slug, the where filter must contain the slug provided by the client in the URL (http://localhost:3000/note/:slug:).

read: (request, reply) => {
  Models.Note
    .findOne({
      where: {
        slug: request.params.slug
      }
    })
    .then((result) => {
      reply(result);
    });
},

As in the previous function, we will just return the result, which is going to be an object containing the note information. The views are going to be used once we build them in the Building the Views section.

The “update” function

To update a note, we use the update method on our model. It takes two objects, the new values that we are going to replace and the options containing a where filter with the note slug, which is the note that we are going to update.

update: (request, reply) => {
  const values = {
    title: request.payload.noteTitle,
    description: request.payload.noteDescription,
    content: request.payload.noteContent
  };

  const options = {
    where: {
      slug: request.params.slug
    }
  };

  Models.Note
    .update(values, options)
    .then(() => {
      Models.Note
        .findOne(options)
        .then((result) => {
          reply(result);
        });
    });
},

After updating our data, since our database won’t return the updated note, we can find the modified note again to return it to the client, so we can show the updated version as soon as the changes are made.

The “delete” function

The delete controller will remove the note by providing the slug to the destroy function of our model. Then, once the note is deleted, we redirect to the home page. To accomplish this, we use the redirect function of the Hapi.js reply object.

delete: (request, reply) => {
  Models.Note
    .destroy({
      where: {
        slug: request.params.slug
      }
    })
    .then(() => reply.redirect('/'));
}

Using the Note controller in our routes

At this point, we should have our note controller file ready with all the CRUD actions. But to use them, we have to include it in our routes file.

First, let’s import our controller at the top of the routes.js file:

const Note = require('./controllers/note');

We have to replace each hander with our new functions, so we should have our routes file as follows:

{
  method: 'POST',
  path: '/note',
  handler: Note.create,
  config: {
    description: 'Adds a new note'
  }
},
{
  method: 'GET',
  path: '/note/{slug}',
  handler: Note.read,
  config: {
    description: 'Gets the content of a note'
  }
},
{
  method: 'PUT',
  path: '/note/{slug}',
  handler: Note.update,
  config: {
    description: 'Updates the selected note'
  }
},
{
  method: 'GET',
  path: '/note/{slug}/delete',
  handler: Note.delete,
  config: {
    description: 'Deletes the selected note'
  }
},

Note: we are including our functions without () at the end, that is because we are referencing our functions without calling them.

Building the Views

At this point, our site is receiving HTTP calls and responding with JSON objects. To make it useful to everybody we have to create the pages that render our information in a nice way.

In this example, we are going to use the Pug templating language, although this is not mandatory and we can use other languages with Hapi.js. Also, we are going to use the Vision plugin to enable the view functionality in our server.

Note: If you’re not familiar with Pug (formerly Jade), see our Jade Tutorial for Beginners.

You can install the packages with the following command:

npm install -s vision pug

The note component

First, we are going to build our note component that is going to be reused across our views. Also, we are going to use this component in some of our controller functions to build a note on the fly in the back-end to simplify the logic on the client.

Create a file in lib/views/components called note.pug with the following content:

article
  h2: a(href=`/note/${note.slug}`)= note.title
  small Published on #{note.date}
  p= note.content

It is composed of the title of the note, the publication date and the content of the note.

The base layout

The base layout contains the common elements of our pages; or in other words, for our example, everything that is not content. Create a file in lib/views/ called layout.pug with the following content:

doctype html
html(lang='en')
  head
    meta(charset='utf-8')
    meta(http-equiv='x-ua-compatible' content='ie=edge')
    title= page
    meta(name='description' content=description)
    meta(name='viewport' content='width=device-width, initial-scale=1')

    link(href='https://fonts.googleapis.com/css?family=Gentium+Book+Basic:400,400i,700,700i|Ubuntu:500' rel='stylesheet')
    link(rel='stylesheet' href='/styles/main.css')
  body
    block content

    script(src='https://code.jquery.com/jquery-3.1.1.min.js' integrity='sha256-hVVnYaiADRTO2PzUGmuLJr8BLUSjGIZsDYGmIJLv2b8=' crossorigin='anonymous')
    script(src='/scripts/jquery.modal.js')
    script(src='/scripts/main.js')

The content of the other pages will be loaded in place of block content. Also, note that we will display a page variable in the title element, and a description variable in the meta(name='description') element. We will create those variables in our routes later.

We also include, at the bottom of the page, three JS files, jQuery, jQuery Modal and a main.js file which will contain all of our custom JS code for the front-end. Be sure to download those packages and put them in a static/public/scripts/ directory. We are going to make them public in the Serving Static Files section.

The home view

On our home page, we will show a list containing all the notes in our database and a button that will show a modal window with a form that allows us to create a new note via Ajax.

Create a file in lib/views called home.pug with the following content:

extends layout

block content
  header(container)
    h1 Notes Board

    nav
      ul
        // This will show a modal window with a form to send new notes
        li: a(href='#note-form' rel='modal:open') Publish

  main(container).notes-list
    // We loop over all the notes received from our controller rendering our note component with each entry
    each note in data.notes
      include components/note

  // Form to add a new note, this is used by our controller `create` function.
  form(action='/note' method='POST').note-form#note-form
    p: input(name='noteTitle' type='text' placeholder='Title…')
    p: input(name='noteDescription' type='text' placeholder='Short description…')
    p: textarea(name='noteContent') Write here the content of the new note…
    p._text-right: input(type='submit' value='Submit')

The note view

The note page is pretty similar to the home page, but in this case, we show a menu with options specific to the current note, the content of the note and the same form as in the home page but with the current note information already filled, so it’s there when we update it.

Create a file in lib/views called note.pug with the following content:

extends layout

block content
  header(container)
    h1 Notes Board

    nav
      ul
        li: a(href='/') Home
        li: a(href='#note-form' rel='modal:open') Update
        li: a(href=`/note/${note.slug}/delete`) Delete

  main(container).note-content
    include components/note

  form(action=`/note/${note.slug}` method='PUT').note-form#note-form
    p: input(name='noteTitle' type='text' value=note.title)
    p: input(name='noteDescription' type='text' value=note.description)
    p: textarea(name='noteContent')= note.content
    p._text-right: input(type='submit' value='Update')

The JavaScript on the client

To create and update notes we use the Ajax functionality of jQuery. Although this is not strictly necessary, I feel it provides a better experience for the user.

This is the content of our main.js file in the static/public/scripts/ directory:

$('#note-form').submit(function (e) {
  e.preventDefault();

  var form = {
    url: $(this).attr('action'),
    type: $(this).attr('method')
  };

  $.ajax({
    url: form.url,
    type: form.type,
    data: $(this).serialize(),
    success: function (result) {
      $.modal.close();

      if (form.type === 'POST') {
        $('.notes-list').prepend(result);
      } else if (form.type === 'PUT') {
        $('.note-content').html(result);
      }
    }
  });
});

Every time the user submits the form in the modal window, we get the information from the form elements and send it to our back-end, depending on the action URL and the method (POST or PUT). Then, we will get the result as a block of HTML containing our new note data. When we add a note, we will just add it on top of the list on the home page, and when we update a note we replace the content for the new one in the note view.

Adding support for views on the server

To make use of our views, we have to include them in our controllers and add the required settings.

In our server.js file, let’s import the Node Path utility at the top of the file, since we are using it in our code to indicate the path of our views.

const Path = require('path');

Now, replace the server.route(Routes); line with the following code block:

server.register([
  require('vision')
], (err) => {
  Hoek.assert(!err, err);

  // View settings
  server.views({
    engines: { pug: require('pug') },
    path: Path.join(__dirname, 'lib/views'),
    compileOptions: {
      pretty: false
    },
    isCached: Settings.env === 'production'
  });

  // Add routes
  server.route(Routes);
});

In the code we have added, we first register the Vision plugin with our Hapi.js server, which is going to provide the view functionality. Then, we add the settings for our views—like the engine that we are going to use and the path where the views are located. At the end of the code block, we add again our routes.

This will make work our views on the server, but we still have to declare the view that we are going to use for each route.

Setting the home view

Open the lib/controllers/home.js file and replace the reply(result); line with the following:

reply.view('home', {
  data: {
    notes: result
  },
  page: 'Home—Notes Board',
  description: 'Welcome to my Notes Board'
});

After registering the Vision plugin, we now have a view method available on the reply object, we are going to use it to select the home view in our views directory and to send the data that is going to be used when rendering the views.

In the data that we provide to the view, we also include the page title and a meta description for search engines.

Setting the note view: create function

Right now, every time we create a note we get a JSON object from the server to the client. But since we are doing this process with Ajax, we can send the new note as HTML ready to be added to the page. To do this, we render the note component with the data we have. Replace the line reply(result); with the following code block:

// Generate a new note with the 'result' data
const newNote = Pug.renderFile(
  Path.join(__dirname, '../views/components/note.pug'),
  {
    note: result
  }
);

reply(newNote);

We use the renderFile method from Pug to render the note template with the data we just received from our model.

Setting the note view: read function

When we enter a note page, we should get the note template with the content of our note. To do this, we have to replace the reply(result); line with this:

reply.view('note', {
  note: result,
  page: `${result.title}—Notes Board`,
  description: result.description
});

As with the home page, we select a view as the first parameter and the data that we are going to use as the second one.

Setting the note view: update function

Every time we update a note, we will reply similarly to when we create new notes. Replace the reply(result); line with the following code:

// Generate a new note with the updated data
const updatedNote = Pug.renderFile(
  Path.join(__dirname, '../views/components/note.pug'),
  {
    note: result
  }
);

reply(updatedNote);

Note: The delete function doesn’t need a view, since it will just redirect to the home page once the note is deleted.

Serving Static Files

The JavaScript and CSS files that we are using on the client side are provided by Hapi.js from the static/public/ directory. But it won’t happen automatically; we have to indicate to the server that we want to define this folder as public. This is done using the Inert package, which you can install with the following command:

npm install -s inert

In the server.register function inside the server.js file, import the Inert plugin and register it with Hapi like this:

server.register([
  require('vision'),
  require('inert')
], (err) => {

Now we have to define the route where we are going to provide the static files, and their location on our server’s filesystem. Add the following entry at the end of the exported object in routes.js:

{
  // Static files
  method: 'GET',
  path: '/{param*}',
  handler: {
    directory: {
      path: Path.join(__dirname, '../static/public')
    }
  },
  config: {
    description: 'Provides static resources'
  }
}

This route will use the GET method and we have replaced the handler function with an object containing the directory that we want to make public.

You can find more information about serving static content in the Hapi.js documentation.

Note: Check the Github repository for the rest of the static files, like the main stylesheet.

Conclusion

At this point, we have a very basic Hapi.js application using the MVC architecture. Although there are still things that we should take care of before putting our application in production (e.g. input validation, error handling, error pages, etc.) this should work as a foundation to learn and build your own applications.

If you would like to take this example a bit further, after finishing all the small details (not related the architecture) to make this a robust application, you could implement an authentication system so only registered users are able to publish and edit notes. But your imagination is the limit, so feel free to fork the application repository, experiment with your ideas and let me know what you create in the comments below!

This article was peer reviewed by Mark Brown and Vildan Softic. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!

  • I agree, it is pretty simple and it will probably grow a lot in a production site, but covers the fundamentals and it would be very hard to cover a big application in one article (this one was already relatively large).

    The organization in this example can work as a base, and then as you grow you can decide how to organize things depending on your needs. And your idea about the services folder sounds like a good idea to me as well.

  • Check if the `db` directory exists, the `database.sqlite` file would be created there.

  • christoph88

    I got it fixed, my index.js file was not in the models folder so sequalise wouldn’t get loaded. Next time when I am doing tutorials I’ll run the server in between to debug.

    • Great, I’m glad you were able to fix it.

Recommended
Sponsors
Get the latest in JavaScript, once a week, for free.