This is a step-by-step presentation of how to build an initially simple web app using the fullstack JavaScript solution provided by MEAN. We will start off with something very basic and then try to add new features and improvements on the way.
Why choose MEAN?
First of all, MEAN stands for Mongo-Express-Angular-Node and it's a combination of JavaScript frameworks and APIs that help building web apps. One of its main advantages is that it allows fast development and offers loads of options in what regards tools, plugins, third-party libraries etc. I will not dive deep into explaining what it is or whether you should use it or not, as there's already plenty of material for that on the internet. I've already been convinced of its usefulness and I'll try as much as possible to share some of my experiences in this tutorial.
1. Getting Started
The notes app that we are going to build is very similar to Google Keep. It basically allows you to add a note that might sum up an idea, a to-do, a reminder or any kind of other information. The app allows you to label/group your notes and it also features the possibility of searching through your notes. Later on we will see how we can personalize it by adding user authentication and other neat features.
1.1 Preparation
Firstly, you should have a basic understanding of JavaScript. But don't worry very much about that right now, as most of this stuff will be pretty straight-forward. Secondly, you'll have to choose your development tools. I personally use Ubuntu 14.04 and Webstorm 10, but feel free to choose anything you like, as MEAN.js is available on most platforms and there are plenty of other IDEs that allow support for it.
Among the requirements before starting development would be the following:
- git - download and install it as the code for this tutorial is available on GitHub and you might need it if you stumble on your way
- Node.js and MongoDB - these is your BE (backend) - node will be your web-server and mongo is the database
*As a side-note, when you install Node.js you also install npm (Node package manager) which is used to download libraries and manage dependencies. Given that npm manages your BE, a similar build tool named Bower will be used to manage your FE (frontend).
Follow the steps and install the up-mentioned requirements. When you're ready make sure you've done it right, open a terminal and enter:
npm
This should output something like:
which tells node/npm has been installed. You could then also check if MongoDB is ready to go:
status mongod
This would output something like:
1.2 File → New Project..
If everything went well, open Webstorm and start a new project. Choose the first option ("empty project") since it's better to start from scratch in the beginning. Later on when you get to feel more confident you can use
generators. Name your new project as you wish and then add the following files and folders to obtain the following structure:
As far as I've seen this is the layout most projects will follow. It has the following meaning:
- app - this directory contains your BE sources
- public - here's where you place all your FE sources (JavaScript files, CSS, images, icons etc.)
- .gitignore - when using Git not all sources need to be checked into source control, so it's best to leave out some files (e.g. generated files, downloaded modules, configuration files etc.). If you don't know what to include here, take a look at the one I've checked in for this project.
- bower.json - this is a JSON file that lists all your FE dependencies
- package.json - similar to the previous, here you list your BE dependencies
- README - it's good practice to document your projects (and of course your sources) and if you wish you may also include a license file if you're working on something really important
- server.js - by convention, this file will be used to launch your BE - it's where all the magic happens as we will see later
Now if you're rather lazy you can grab the commit with the empty project from GitHub. Open Webstorm and then go to VCS → Checkout from Version Control. Enter your GitHub credentials and then you should reach something like:
Then, you can easily grab a commit (like
this one) by entering:
git reset --hard fb58046d8b8726f614198a40af1c99f66ae26659
*As a side note, if you ever wish to put some project of yours on GitHub, it's really easy with Webstorm. Go to VCS → Import into Version Control → Share project on GitHub. Enter your login credentials, then add a new repository and click Share. You will be prompted with a dialog with your initial commit, click Ok and then your project should be public on GitHub.
2. The notes app
The app itself revolves around the "note" entity. In the first part we will learn how to build a REST API with Express.JS which is very much like an application server. The API we expose allows the client side to execute CRUD operations on the this entity. We will se how we can interact with MongoDB and how we can deal with error handling. The second part will show how we can use Angular.JS and Bootstrap to interact with the server side and build up the presentation layer.
2.1 The Backend
Our REST API will be a back and forth between router, controller and model. The router defines the API endpoints (URL paths) and links them to the controller methods. The controller is the place where we can put our business logic. In our case we just query the model. The model defines our schema.
Later on, we will make use of the $http module in Angular to call this API from the frontend.
For the purpose of this app we could simply structure our sources into three directories:
Later, when dealing with a larger number of entities, we could organize them in modules, each having this basic structure.
2.1.1 The Model
Before we start developing the backend we define our entities. We will do this by using
mongoose, which is a higher level framework for working with MongoDB. We could've just as easily used the
native Node.js driver, but the advantage of using mongoose would be that it makes it easier to write simple things (such as
CRUD) and allows faster development. More on this
here.
Our first entity "note" will be defined as app/model/note.js:
// import necessary module(s)
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
// define a schema
var noteSchema = new Schema({
title: String,
content: { type: String, required: true },
created: Date,
updated: Date
});
// create a model using the defined schema
var Note = mongoose.model('Note', noteSchema);
// export this to other modules
module.exports = Note;
We simply define the entity using a mongoose schema and then expose it to the other modules. You can find the commit for this
here.
2.1.2 The Controller
When dealing with REST services we focus on two objects:
request and
response. The Express.js framework provides a great foundation for building functionality such as pre/post-processing called
middleware. The controller is a good place to gather your business logic. Here you can access the model and add your custom middleware. (e.g. validations, logging etc.)
var Note = require('../model/note');
var Controller = {};
Controller.create = function (req, res) {
var note = new Note(req.body);
note.save(function (err, saved) {
if (err) {
res.status(400);
res.json({ error: "Error creating " + note.toString(), message: err.toString() });
} else {
res.status(201);
res.json(saved);
}
});
};
Controller.read = function (req, res) {
Note.find(req.params, function (err, found) {
if (err) {
res.status(400);
res.json({ error: "Error finding " + found.toString(), message: err.toString() });
} else {
res.status(200);
res.json(found);
}
});
};
Controller.update = function (req, res) {
var query = { _id: req.params.id };
Note.findOneAndUpdate(query, req.body, { new: true },function (err, found) {
if (err) {
res.status(400);
res.json({ error: "Error updating " + found.toString(), message: err.toString() });
} else {
res.status(200);
res.json(found);
}
});
};
Controller.delete = function (req, res) {
var query = { _id: req.params.id };
Note.remove(query, function (err, found) {
if (err) {
res.status(400);
res.json({ error: "Error deleting " + found.toString(), message: err.toString() });
} else {
res.status(200);
res.end();
}
});
};
module.exports = Controller;
As you may notice, the Note controller simply makes use of the CRUD functionality exposed by our mongoose model. Everything happens asynchronously, so we use callbacks to deal with "incoming" results. Upon receiving a result we send a
status code and the result as a JSON object.
2.1.3 The Router
The role of the router is to delegate requests to the corresponding controller handler.
var express = require('express');
var NoteController = require('../controller/note-ctrl');
var router = express.Router();
router
.post('/note/create', NoteController.create)
.get('/note/read/:title', NoteController.read)
.get('/note/read/:title/:content', NoteController.read)
.put('/note/update/:id', NoteController.update)
.delete('/note/delete/:id', NoteController.delete);
module.exports = router;
You may notice the get and delete requests contain a ":param" in the URL. This acts like a placeholder for data you can send in the
query string. You can then find these parameters in the
request.params object. Alternately, when using a body, you may access it through
request.body.
2.1.4 The main file
At this point we're ready to launch our fully functional REST API. We only need to configure and launch an
express server. For this we will add the following to our server.js file:
// setup modules
var express = require('express');
var bodyParser = require('body-parser');
var mongoose = require('mongoose');
// setup express
var app = express();
// use middleware to parse application/json
app.use(bodyParser.json());
// use custom router
app.use('/', require('./app/route/note-rt'));
// start mongo
mongoose.connect('mongodb://localhost/notes', function (err) {
if (err) console.error('Could not start database: ' + err.toString())
else console.log("Database started at " + new Date())
});
// start server
app.listen(1337, 'localhost', function () {
console.log("Server started at " + new Date())
});
I find it's best to keep things simple, so later on we could move the Express configuration code into a separate tree structure under an app/config directory.
The
package.json file was previously mentioned. All your external library dependencies will be listed here. You can specify development and release dependencies as well as scripts to execute. You can find a very basic package.json in
this commit. Before launching the server you need to enter the following command line:
npm install
This creates a node_modules directory where all your required libraries will be held.
2.1.5 Testing with Postman
First we need to launch the server by right clicking server.js and choosing Run/Debug, by case. You can also do this by command line, this is actually the equivallent of running
node server.js
We can now (manually) test our backend. We do this by using
Postman. Alternately you could use the built-in Webstorm client: Tools → Test RESTful Web Service.
Test adding a note:
*when adding a body to your request you need to add a "Content-Type": "application/json" header
We then retrieve the note:
which yields
Updating a note:
gives:
And finally removing it:
giving:
Here are the commits for
create,
read,
update and
delete.
There are cases in which resources might not be found due to various reasons. You can cover this case by using a middleware placed right after all your custom routes. An updated server.js may be found in
this commit. I've also added a
logger that helps out on desplaying all client requests.
2.2 The Frontend
The Angular approach in building the frontend is to enhance the html by adding custom tags, attributes etc. making it dynamic. Angular is a lot to talk about as it is complex and it can be complicated, so here are some ideas to start with:
- We can use templates to group/reuse html and views to present/manipulate data
- We associate a controller to each view
- We share data between views/controllers by using factories
- We call services to use backend APIs
- We use directives for custom components
We will build our frontend using Angular and design it as a
SPA. At this point you can start off very simple and do everything yourself (which may take some extra time) or choose an easier way (like I did) and grab a
Bootstrap template and then integrate it with Angular. For the purpose of this notes app I picked
this particular theme since it's best suited for our needs.
2.2.1 Adapting the Hydrogen SPA to Angular.js
The hydrogen theme may seem cool but if we want it to "play nice" with Angular, we'll need to make a few adjustments. For those of you who don't really care what it took to make it Angular-ready, you can skip to the next subchapter directly.
When working with such templates, among the first things to do is to gather up all the original's resources (CSS, JS, fonts etc.) and track them such that you can use them as Bower dependencies. This will make your life much easier and as a plus, you won't need to overload your GitHub repository with stuff that already exists somewhere else. For the hydrogen template, a working bower.json would look like this:
{
"name": "notes",
"version": "0.0.1",
"authors": [
"RobyRodriguez"
],
"dependencies": {
"angular": "~1.5.7",
"angular-route": "~1.5.7",
"bootstrap": "~3.3.6",
"font-awesome": "~4.6.3",
"animate.css": "~3.5.1",
"modernizr": "~3.3.1",
"jquery.easing": "~1.3.1",
"waypoints": "~4.0.0",
"magnific-popup": "~0.9.9",
"salvattore": "~1.0.9"
}
}
This way, when running a "bower install" you will have all these frontend libararies gathered up in your bower_components directory from which you can then serve to your client.
Some of the stuff you will not find, in our case hydrogen's CSS which lies in css/style.css. A good idea is to place your own custom CSS separately, in a file such as css/style_extra.css. After integrating this theme, your project structure would now contain the following additions:
The entry point of the frontend is index.html. This is where you place all your styling and necessary libraries references. As a general rule, stylesheets are placed in the <head> tag such that they are loaded first, since this is the first content you see when the page is loaded. At the end of your <body> tag you will put your JavaScript references.
When using Angular you should add the following inside the <body> of your index.html:
<div ng-app="notesApp">
<ng-view></ng-view>
</div>
This identifies the use of "notesApp" module which basically represents your Angular app. The other important tag used here is ng-view, which will act as a placeholder for the different views you use in your app. But more on this later. The last thing you need here is a place to bootstrap your Angular app. In our case this is the js/app.js file which looks like this initially:
angular.module("notesApp", []);
You can find the commit for this subchapter
here.
2.2.2 Adding a note
In the previous section we only managed to build the structure of our website but didn't really accomplish much visually. If you type
http://localhost:1337 you just will get a blank page (with hopefully no console errors).
So we set out with adding a new note. We will add a new view ./public/views/create.html with a form that allows doing so. This should look much like:
<form ng-submit="create()">
<div class="row">
<div class="col-md-12">
<div class="form-group">
<input type="text" class="form-control" placeholder="Enter title..." ng-model="note.title">
</div>
<div class="form-group">
<input type="text" class="form-control" placeholder="Enter (optional) image URL..." ng-model="note.image">
</div>
<div class="form-group">
<textarea class="form-control" cols="30" rows="10" placeholder="Enter content..." ng-model="note.content"></textarea>
</div>
<div class="form-group">
<input type="submit" class="btn btn-primary" value="Add">
</div>
</div>
</div>
</form>
You might have noticed the ng-* attributes. These are the ones which allow binding logic to this view. So the next thing to do is to write a controller that defines this logic.
angular.module('notesApp')
.controller('CreateController', function($scope, $rootScope, $location, NoteService) {
$scope.note = {};
$scope.create = function () {
NoteService.createNote($scope.note, function (err, res) {
if (err) {
$rootScope.error = 'Could not create note';
$rootScope.errorMessage = JSON.stringify(err);
} else {
$rootScope.infoMessage = 'Added note: ' + JSON.stringify(res);
$location.path('/home');
}
});
};
});
As we see, the
note variable, as well as the
create method are placed on our create controller's scope. The note defaults to an empty object upon controller initialization whereas the create method calls a "note-" service, passing the same note object which will then contain user data.
angular.module('notesApp')
.service('NoteService', function($http) {
this.createNote = function (data, next) {
return $http.post('/note/create', data)
.success(function(info){
next(null, info);
})
.error(function (err) {
next(err);
});
};
});
The service makes a POST to our API endpoint. We can use a Node-like "next" callback to pass data back to our controller.
You might have noticed the use of the "root scope" in our create controller. When dealing with server responses it may be a good idea to give some input to the user regarding the result of the current operation (success/fail). As a homework, you could write up a new controller that does that. (hint: write a reusable template that holds each "error", "errorMessage" and "infoMessage")
Finally, we establish a new route for the create action. We will use the route provider module to define it:
angular.module("notesApp", [
'ngRoute'
]).config(function ($routeProvider) {
$routeProvider
.when('/create', {
templateUrl: 'views/create.html',
controller: 'CreateController'
});
});
Now you can enter
http://localhost:1337/#/create in your favorite browser and you should be able to complete the new form:
We can look up the result of this operation by entering
db.notes.find() in the mongo shell. Its output should look like:
You can find the full commit for this section
here.
2.2.3 Reading notes
Before we start reading notes it might be a good idea to add a few to our database. So, as a homework, you should write a script that builds the notes database each time you run it. As a hint, you could use the data from hydrogen's index.html to build a JSON and dump it into the database. If you need help, take a look at
this commit.
Now that we have something to show we will add a new read method to our notes service:
angular.module('notesApp')
.service('NoteService', function($http) {
...
this.readNotes = function (next) {
return $http.get('/note/read')
.success(function(notes){
next(null, notes);
})
.error(function (err) {
next(err);
});
};
});
And then we will create a
factory that stores the notes retrieved by calling the service. As a reminder, we generally use these factories when we need to share data between controllers.
angular.module('notesApp')
.factory('NotesFactory', function(NoteService) {
var notes = [];
return {
getNotes: function(next) {
if (next) {
NoteService.readNotes(function (err, res) {
notes = res;
next(err, res)
})
}
return notes;
}
};
});
We will add a new "home-" view and controller that will also be the default page the user sees. The home controller loads the notes on its scope upon initialization:
angular.module('notesApp')
.controller('HomeController', function($scope, $rootScope, NotesFactory) {
$scope.loadNotes = function () {
// load user notes
NotesFactory.getNotes(function (err, res) {
if (err) {
$rootScope.error = 'Could not load notes';
$rootScope.errorMessage = JSON.stringify(err);
} else {
$scope.notes = res;
}
});
};
$scope.loadNotes();
});
In order to display the notes we will use the
ng-repeat directive. So we add a new view 'views/home.html':
<div ng-include src="'templates/serverResponse.html'"></div>
<div id="fh5co-main" >
<div class="container">
<div class="row">
<div id="fh5co-board">
<div ng-repeat="note in notes track by $index">
<div class="item">
<div class="animate-box" style="opacity: inherit;">
<a href="#{{ 'nt-viewer-' + $index }}" class="image-popup fh5co-board-img">
<img ng-src="{{ note.image }}">
<div id="{{ 'nt-viewer-' + $index }}" class="note-viewer mfp-hide">
</div>
</a>
</div>
<div class="fh5co-item-title">{{ note.title }}</div>
<div class="fh5co-desc">{{ note.content }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
Finally, we add the new route to app.js:
...
.when('/home', {
templateUrl: 'views/home.html',
controller: 'HomeController',
controllerAs: 'hm'
})
...
The commit for displaying the notes is
here.
You might have noticed the hydrogen animations are missing. No worry because we will integrate them step-by-step into our Angular app. We will start with waypoints. In order for this to work, we will refactor the original initialization function into an
Angular directive. As for salvattore, it will be necessary to postpone its launch until the home controller is done loading its notes. The code for the waypoints directive should look like:
angular.module('notesApp')
.directive('waypoint', function () {
return {
link: function (scope, element) {
$(element).waypoint( function( direction ) {
if( direction === 'down' && !$(this).hasClass('animated') ) {
$(this.element).addClass('bounceIn animated');
}
} , { offset: '75%' } );
}
};
});
As for salvattore, we will add an init-method in main.js to call in our home controller:
...
return {
initHome: function () {
// this hack is needed to get allow angular finish its housekeeping
setTimeout(window.salvattore.init, 0);
}
}
...
For more information on why the above call is needed check
this source. You can find the commit for the above
here.
2.2.4 Updating a note
For updating notes we could easily use the existing hydrogen popup. The only thing we need to do is write a new directive to integrate it into our Angular app:
angular.module('notesApp')
.directive('magnificPopup', function () {
return {
link: function (scope, element) {
$(element).magnificPopup({
type: 'inline',
removalDelay: 300,
mainClass: 'mfp-with-zoom',
...
As you see, it contains hydrogen's original initialization code of
magnific-popup. We then apply this directive to the <a> link in home.html. This will launch the popup when clicking the note image. We could then use this popup to build a form that allows editing/removing a note.
To go a step further, we will write a new factory that allows sharing the selected note with the new editor view:
angular.module('notesApp')
.factory('NoteViewerFactory', function() {
var note = {};
return {
setNote: function(data) {
note = data;
},
getNote: function() {
return note;
}
};
});
To make this work we need to adjust the home view:
...
<a href="#{{ 'nt-viewer-' + $index }}" ng-click="setSelection(note)" class="image-popup fh5co-board-img" magnific-popup>
...
and its corresponding controller:
angular.module('notesApp')
.controller('HomeController', function($scope, $rootScope, $window, NotesFactory, NoteViewerFactory) {
$scope.setSelection = function (note) {
NoteViewerFactory.setNote(note)
};
...
The next step would be to create a new template 'templates/noteViewer.html' which will contain the previously mentioned form:
<div ng-include src="'templates/serverResponse.html'"></div>
<div class="container" ng-controller="NoteViewerController">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<h2>Note viewer</h2>
<div class="fh5co-spacer fh5co-spacer-sm"></div>
<form ng-submit="update()">
<div class="row">
<div class="col-md-12">
<div class="form-group">
<input type="text" class="form-control" placeholder="Enter title..." ng-model="note.title">
</div>
<div class="form-group">
<input type="text" class="form-control" placeholder="Enter (optional) image URL..." ng-model="note.image">
</div>
<div class="form-group">
<textarea class="form-control" cols="30" rows="10" placeholder="Enter content..." ng-model="note.content"></textarea>
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary">
<i class="icon-save"></i> Update
</button>
<button type="button" ng-click="delete()" class="btn btn-danger">
<i class="icon-trash"></i> Delete
</button>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
The note viewer controller:
angular.module('notesApp')
.controller('NoteViewerController', function($scope, $rootScope, $route, NoteViewerFactory, NotesFactory, NoteService) {
$scope.update = function () {
var selected = NoteViewerFactory.getNote();
NoteService.updateNote(selected._id, selected, function (err, res) {
if (err) {
$rootScope.error = 'Could not update note';
$rootScope.errorMessage = JSON.stringify(err);
} else {
$rootScope.infoMessage = 'Updated note: ' + JSON.stringify(res);
$.magnificPopup.instance.close();
}
});
};
});
And finally, the update note service method:
angular.module('notesApp')
.service('NoteService', function($http) {
...
this.updateNote = function (noteId, data, next) {
return $http.put('/note/update/' + noteId, data)
.success(function(info){
next(null, info);
})
.error(function (err) {
next(err);
});
};
});
As a result of the above, when clicking a note image, the following popup should appear:
which should allow live-editing of the selected note.
You can find the commit for this section
here.
2.2.5 Deleting a note
For deleting a note we will add an extra button to the editor view:
<button type="button" ng-click="delete()" class="btn btn-danger">
<i class="icon-trash"></i> Delete
</button>
Which will render to:
Similar to update, we add a new delete method to the note viewer controller:
$scope.delete = function() {
var selected = NoteViewerFactory.getNote();
NoteService.deleteNote(selected._id, function(err) {
if (err) {
$rootScope.error = 'Could not delete note';
$rootScope.errorMessage = JSON.stringify(err);
} else {
$rootScope.infoMessage = 'Deleted note: ' + JSON.stringify(selected);
$route.reload();
$.magnificPopup.instance.close();
}
});
};
And the coresponding service call:
angular.module('notesApp')
.service('NoteService', function($http) {
...
this.deleteNote = function (noteId, next) {
return $http.delete('/note/delete/' + noteId)
.success(function(info){
next(null, info);
})
.error(function (err) {
next(err);
});
};
});
The result of deleting a note should look like:
The commit for this section is available
here.
The original hydrogen template also contained some headers and a fold menu. We will also integrate these into our Angular app. This will be pretty much straight-forward, as we write templates for each. The header:
<header id="fh5co-header" role="banner" ng-controller="HeaderController">
<div class="container">
<div class="row">
<div class="col-md-12">
<a class="fh5co-menu-btn js-fh5co-menu-btn">Menu <i class="icon-menu"></i></a>
<a class="navbar-brand" href="#/home">Notes</a>
</div>
</div>
</div>
</header>
The footer:
<footer id="fh5co-footer">
<div class="container">
<div class="row row-padded">
<div class="col-md-12 text-center">
<p class="fh5co-social-icons">
<a href="#"><i class="icon-twitter"></i></a>
<a href="#"><i class="icon-facebook"></i></a>
<a href="#"><i class="icon-instagram"></i></a>
<a href="#"><i class="icon-dribbble"></i></a>
<a href="#"><i class="icon-youtube"></i></a>
</p>
<p><small>© Hydrogen Free HTML5 Template. All Rights Reserved. <br>Designed by: <a href="http://freehtml5.co/" target="_blank">FREEHTML5.co</a></small></p>
</div>
</div>
</div>
</footer>
And the fold menu:
<div id="fh5co-offcanvass" ng-controller="FoldMenuController">
<a class="fh5co-offcanvass-close">Menu <i class="icon-cross js-fh5co-offcanvass-close"></i> </a>
<h1 class="fh5co-logo"><a class="navbar-brand" href="index.html">Notes</a></h1>
<ul class="fh5co-fold-menu">
<li ng-class="{active: isActive('/home')}"><a href="#/home">Home</a></li>
<li ng-class="{active: isActive('/create')}"><a href="#/create">Create</a></li>
</ul>
<h3 class="fh5co-lead">Connect with us</h3>
<p class="fh5co-social-icons">
<a href="#"><i class="icon-twitter"></i></a>
<a href="#"><i class="icon-facebook"></i></a>
<a href="#"><i class="icon-instagram"></i></a>
<a href="#"><i class="icon-dribbble"></i></a>
<a href="#"><i class="icon-youtube"></i></a>
</p>
</div>
We also refactor the original fold-menu functionality into the main.js module and call the necessary methods from the corresponding controllers. You can find the commit for this
here.
3. Running the app
At this point our MEAN.JS app ist just about ready to go. One thing left to do would be to prevent server crashes caused by uncaught errors in the main event loop.
This can be solved easily by:
process.on('uncaughtException', function (err) {
console.log("Uncaught exception at " + new Date());
console.error(err);
});
This prevents the server process from exiting and it should be used only as a
temporary solution. In general, error cases should be dealt with properly.
Another step we should take before running the app is to load the database with some test data. We do this by starting Node.js (ie. launching node.exe) with ./migration/loader.js as an argument. This will fill the "notes" collection of the "notes" database with the data found in ./migration/testdata.json.
We run the app by launching the server. This is done by starting Node.js with server.js as an argument, or Right click on 'server.js' → Run (from WebStorm). You can then start a browser instance and go to
http://localhost:1337/
4. Conclusions
We now know the steps required to build a simple and working web application in MEAN.JS. There is much more to talk about and things can go way deeper than that but at the very simplest this would be it.
5. Follow-up
My intention is to expand on this subject. Among the topics I would like to cover in the upcoming posts would be the following:
- testing
- build management
- authentication + passport
- cloud9 + mongolab
- websockets
- migrating to react
- migrating to spring