How to build a M.E.A.N web application


https://form.io

<form.io> provides developers an easy drag & drop interface that creates both forms and the REST API's in one easy step!

Presentation Materials

What is a M.E.A.N web application?

  • M:
    MongoDB - A NoSQL database powered by JavaScript.
  • E:
    ExpressJS - A Node.js application framework.
  • A:
    AngularJS - A front-end javascript application framework.
  • N:
    Node.js - A server-side javascript application engine.

Let's build an App!

  • A movie trailer application
  • Full CRUD capabilities
  • Total separation between API server and AngularJS front-end.

Getting Started

  • Install Homebrew
    ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
  • Install NodeJS
    brew install node
  • Install MongoDB
    brew install mongodb
  • Install GruntCLI
    npm install -g grunt-cli
  • Install Bower
    npm install -g bower
  • Install Compass
    gem install compass
  • Run MongoDB
    mongod

Building the Server

"API first"

Setup Node.js project

Initialize the app

mkdir server
cd server
npm init

Only provide the Name. Press enter through all the questions.

name: (server) MeanApp
version: (1.0.0)
description: A M.E.A.N application.
entry point: (index.js)
test command:
git repository:
keywords:
author:
license: (ISC)
About to write to /Users/travistidwell/Documents/projects/meanapp/server/package.json:

{
	"name": "MeanApp",
	"version": "1.0.0",
	"description": "A M.E.A.N application.",
	"main": "index.js",
	"scripts": {
		"test": "echo \"Error: no test specified\" && exit 1"
	},
	"author": "",
	"license": "ISC"
}
							

Installing Node.js modules


							npm install --save express
npm install --save mongoose
npm install --save resourcejs
npm install --save method-override
npm install --save body-parser
npm install --save lodash

Writing the bootstrap

server/index.js
var express = require('express');
var mongoose = require('mongoose');
var bodyParser = require('body-parser');
var methodOverride = require('method-override');
var _ = require('lodash');
// Create the application.
var app = express();
// Add Middleware necessary for REST API's
app.use(bodyParser.urlencoded({extended: true}));
app.use(bodyParser.json());
app.use(methodOverride('X-HTTP-Method-Override'));
// Connect to MongoDB
mongoose.connect('mongodb://localhost/meanapp');
mongoose.connection.once('open', function() {
								  console.log('Listening on port 3000...');
  app.listen(3000);
});

Running the app

Shell
  node index.js
						
Output
  Listening on port 3000...
						

If you to to http://localhost:3000 you will see the server is running, but nothing shows up. This is expected.

Adding Middleware

Let's add CORS support for RESTful interfaces.

server/index.js
...
...
app.use(methodOverride('X-HTTP-Method-Override'));

// CORS Support
app.use(function(req, res, next) {
  res.header('Access-Control-Allow-Origin', '*');
  res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE');
  res.header('Access-Control-Allow-Headers', 'Content-Type');
  next();
});

mongoose.connect('mongodb://localhost/meanapp');
...
...
						

Using app.use for content routes

server/index.js
...
...
app.use('/hello', function(req, res, next) {
  res.send('Hello World!');
  next();
});
...
...
						

Now go to http://localhost:3000/hello in your browser.

This is the fundamental idea at registering REST endpoints at certain paths.

Creating a Model

New file @ server/models/Movie.js
var mongoose = require('mongoose');

// Create the MovieSchema.
var MovieSchema = new mongoose.Schema({  title: {
    type: String,
    required: true
  },
  url: {
    type: String,
    required: true
  }
});
// Export the model.
module.exports = mongoose.model('movie', MovieSchema);

Index of Models

New file @ server/models/index.js
module.exports = {
  movie: require('./Movie')
};

Loading the Models

server/index.js
...
...
mongoose.connection.once('open', function() {

  // Load the models.
  app.models = require('./models/index');

...
...

Creating a Controller

New file @ server/controllers/MovieController.js
var Resource = require('resourcejs');
module.exports = function(app, route) {
  // Setup the controller for REST;
  Resource(app, '', route, app.models.movie).rest();
  // Return middleware.
  return function(req, res, next) {
    next();
  };
};

Creating the routes

New file @ server/routes.js
module.exports = {
  'movie': require('./controllers/MovieController')
};

Registering the routes

server/index.js
...
...

app.models = require('./models/index');

// Load the routes.
var routes = require('./routes');_.each(routes, function(controller, route) {  app.use(route, controller(app, route));
});

...
...

Check your code

https://github.com/travist/meanapp/tree/resourcejs/server

Run the code

cd server
node index.js

Use Postman to test

Building the Client

in AngularJS

Bootstrapping the client with Yeoman

http://yeoman.io/

mkdir clientcd clientnpm install -g yonpm install -g generator-angularyo angular

Generate the movies route

yo angular:route movies

Add Movies to the navigation

client/app/index.html

							

Remove controllerAs statements

These are only needed for nested views.

app/app.js
.when('/movies', {
  templateUrl: 'views/movies.html',
  controller: 'MoviesCtrl',
})
						

Create a table list of Movies

app/views/movies.html<table class="table table-striped">
  <thead>
    <th>Title</th>
    <th>URL</th>
  </thead>
  <tbody>    <tr ng-repeat="movie in movies">      <td>{{ movie.title }}</td>
      <td>{{ movie.url }}</td>    </tr>
  </tbody>
</table>
						

Add movies to the scope

app/scripts/controllers/movies.js
angular.module('clientApp')
.controller('MoviesCtrl', function ($scope) {
  $scope.movies = [
    {
      title: 'A New Hope',
      url: 'http://youtube.com/embed/1g3_CFmnU7k'
    },
    {
      title: 'The Empire Strikes Back',
      url: 'http://youtube.com/embed/96v4XraJEPI'
    },
    {
      title: 'Return of the Jedi',
      url: 'http://youtube.com/embed/5UfA_aKBGMc'
    }
  ];
});
						

Let's hook up $scope.movies with our server.

Use Bower to add Restangular to your project


bower install --save restangular
						

Verify it adds it to app/index.html



						

Adding and Configuring Restangular

app/scripts/app.js
angular
.module('clientApp', [
  'ngRoute',
  'restangular'
])
.config(function (
  $routeProvider,  RestangularProvider
) {
  RestangularProvider.setBaseUrl('http://localhost:3000');
  ...
  ...

Adding Movie factories

app/scripts/app.js
.factory('MovieRestangular', function(Restangular) {
  return Restangular.withConfig(function(RestangularConfigurer) {
    RestangularConfigurer.setRestangularFields({
      id: '_id'
    });
  });
})
.factory('Movie', function(MovieRestangular) {
  return MovieRestangular.service('movie');
})
						

Query the list of movies

app/controllers/movies.js
.controller('MoviesCtrl', function ($scope, Movie) {
  $scope.movies = Movie.getList().$object;
});
						

Adding CRUD capabilities


yo angular:route movie-add --uri=create/movie
yo angular:route movie-view --uri=movie/:id
yo angular:route movie-delete --uri=movie/:id/delete
yo angular:route movie-edit --uri=movie/:id/edit
						

Remove controllerAs statements

These are only needed for nested views.

app/app.js
.when('/create/movie', {
  templateUrl: 'views/movie-add.html',
  controller: 'MovieAddCtrl'
})
.when('/movie/:id', {
  templateUrl: 'views/movie-view.html',
  controller: 'MovieViewCtrl'
})
.when('/movie/:id/delete', {
  templateUrl: 'views/movie-delete.html',
  controller: 'MovieDeleteCtrl'
})
.when('/movie/:id/edit', {
  templateUrl: 'views/movie-edit.html',
  controller: 'MovieEditCtrl'
})
						

CREATE

app/views/movie-add.html
app/scripts/controllers/movie-add.js
.controller('MovieAddCtrl', function ($scope, Movie, $location) {
  $scope.movie = {};
  $scope.saveMovie = function() {
    Movie.post($scope.movie).then(function() {
      $location.path('/movies');
    });
  };
});
						

READ

app/views/movie-view.html

{{ movie.title }}

{{ movie.url }}

app/scripts/controllers/movie-view.js
.controller('MovieViewCtrl', function (
  $scope,
  $routeParams,
  Movie
) {
  $scope.viewMovie = true;
  $scope.movie = Movie.one($routeParams.id).get().$object;
});
						

UPDATE

app/views/movie-edit.html

						
app/scripts/controllers/movie-edit.js
.controller('MovieEditCtrl', function (
  $scope,
  $routeParams,
  Movie,
  $location
) {
  $scope.editMovie = true;
  $scope.movie = {};
  Movie.one($routeParams.id).get().then(function(movie) {
    $scope.movie = movie;
    $scope.saveMovie = function() {
      $scope.movie.save().then(function() {
        $location.path('/movie/' + $routeParams.id);
      });
    };
  });
});
						

DELETE

app/views/movie-delete.html

Are you sure you wish to delete the movie {{ movie.title }}?

app/scripts/controllers/movie-delete.js
.controller('MovieDeleteCtrl', function (
  $scope,
  $routeParams,
  Movie,
  $location
) {
  $scope.movie = Movie.one($routeParams.id).get().$object;
  $scope.deleteMovie = function() {
    $scope.movie.remove().then(function() {
      $location.path('/movies');
    });
  };
  $scope.back = function() {
    $location.path('/movie/' + $routeParams.id);
  };
});
						

Creating a navigation tabs

Create: app/views/movie-nav.html

						
app/views/movie-view.html

{{ movie.title }}

{{ movie.url }}

app/views/movie-edit.html


						

Adding links to the movies page.

app/views/movies.html
 Create Movie

Title URL Operations
{{ movie.title }} {{ movie.url }}

Adding a YouTube Player directive

app/scripts/app.js
.directive('youtube', function() {
  return {
    restrict: 'E',
    scope: {
      src: '='
    },
    templateUrl: 'views/youtube.html'
  };
}).filter('trusted', function ($sce) {
  return function(url) {
    return $sce.trustAsResourceUrl(url);
  };
});
						

Adding a YouTube Player directive

app/views/youtube.html
app/views/movie-view.html

{{ movie.title }}

Amazing!

THANK YOU!