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’ll have a hard time as the complexity increases. There’s 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 withnpm 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.
Continue reading %How to Build and Structure a Node.js MVC Application%
by James Kolce via SitePoint
No comments:
Post a Comment