Skip to content

tvrbo-pro/heroku-bp

Repository files navigation

Heroku herokubp

This project is a boilerplate of a web site built on AngularJS, Node/Express, MongoDB and Gulp.

Featuring:

  • Jade templating engine
  • SASS
  • Angular gettext translations
  • Generates PoEdit-ready translation templates
  • Angular ng-annotate for minified code
  • Angular template caching
  • Automatic make on push to heroku
  • Bower support
  • Live reload

Get Started

Clone the project into your computer:

$ git clone https://github.com/TvrboPro/heroku-bp.git .
$ cd herokubp

Dependencies

To start the application with fail safety, you will need to install pm2 on the server.

$ sudo npm install -g pm2

To manage the app in development and production environments, we will make use of gulp.

$ sudo npm install -g gulp

Finally, we will need to install the local dependencies of the project.

$ npm install

Usage

To manage the app, the following actions are available

$ cd herokubp
$ gulp

Usage:

 $ gulp make          Compiles the app from src/ to www/
 $ gulp debug         Compile, start the app locally and reload with 	Nodemon
 $ gulp po:extract    Generate the translation template (po/	template.pot)
 $ gulp po:compile    Generate the files for each language (po/*.js)
  
 $ gulp bower         Downloads the dependencies from bower.json to 	src/vendor
 $ gulp readme        Show the readme in the browser
  
 $ gulp start         Start the server as a daemon (implies gulp make)
 $ gulp restart       Restart the server (implies gulp make)
 $ gulp stop          Stop the server

The first three ones are intended for the development environment. The last three ones are intended to manage the application in a server.

Folder structure

These are the files and folders of the project

README.md               The file you are reading
package.json            Node dependencies
bower.json              HTML/JS/CSS dependencies
gulpfile.js             Scripts to compile and manage the application
server.js               The server main executable

controllers/            JS files containing the logic of the backend
models/                 Data models (bound to MongoDB collections)
src/                    Source code of the HTML application (compiled to 'www')
www/                    (Compiled folder from where the web is served)

node_modules/           (managed by npm)
test/                   Folder with the test suites

Development workflow

Backend

Configuring the project

The default parameters of the server are defined in controllers/config.js

var defaults = {
    IS_PRODUCTION: false,
    
    APP_NAME: 'Tvrbo App',
    DOMAIN: 'boilerplate.herokuapp.com',
    ENSURE_WWW: false,

    HTTP_PORT: process.env.PORT || 8080,
    HTTPS_PORT: 8443,

    USE_HTTP: true,
    USE_HTTPS: false,
    USE_MONGODB: true,
    USE_PRERENDER: false,
    USE_CACHE: true,
    USE_URL_ALIAS: false,
    ALLOW_CORS: false,

    MONGODB_HOST: 'mongo-server.com',
    MONGODB_PORT: '1234',
    MONGODB_DB: 'dbname',
    MONGODB_USER: '',
    MONGODB_PASSWORD: '',

    HTTP_USER: '',
    HTTP_PASSWORD: '',

    ADMIN_HTTP_USER: '',
    ADMIN_HTTP_PASSWORD: '',


    KEY_FILE: '/etc/pki/tls/private/localhost.key',
    CERT_FILE: '/etc/pki/tls/certs/localhost.crt',
    CA_FILE: '/etc/pki/tls/certs/ca-bundle.crt',

    PRERENDER_URL: 'http://localhost:3000'
};

They can be modified right in the file or they can be averriden via environment variables:

$ HTTP_PORT=3000 gulp debug
Notes:
  • HTTP and HTTPS ports must be greater than 1024 (unless you plan to run the app with root privileges)
    • Normally you'll set up a proxy server (Apache/nginx) binding the ports 80/443 to the ones you define above
  • If the MongoDB user/password are empty, the server will attempt to connect without authentication
  • Unless you set an HTTP user/password, no HTTP authentication will be requested
  • If USE_HTTPS is false, the keyFile/certFile files will be ignored
    • If a CA_FILE is not provided, only the private/public keys will be used

Creating a model

Let's create (or edit) a file inside the models/ folder with the data schema that we want to store on the DB. For example user.js

// User Model
var mongoose = require('mongoose'),
   Schema = mongoose.Schema,
   ObjectId = Schema.ObjectId;

var userSchema = new Schema({
    state: {
        type: 'String',
        required: true,
        enum: ['active', 'temporary', 'inactive'],
        lowercase: true
    },
    name: String,
    lastName: String,
    username: String,
    nick: String,
    email: String,
    score: { type: Number, min: 0, max: 10000, required: true },
    created: Date,
    notificacions: [ String ]
}, {
    collection: 'users'
});

module.exports = mongoose.model('User', userSchema);

We just created a model called User which will use the users collection on the server.

Using the model in an API call

In the file controllers/server.api.js let's import our new model. Next, we will export a function that generates a list of all the registered users.

var User = require('../models/user.js');

exports.listUsers = function(req, res) {
  User.find()
  .where('state').equals('active')
  .sort('-score')
  .exec(function(err, users){
    if(err)
      res.send({error: err});
    else
      res.send(users);
  });
};

This callback will send a JSON response with a list of the users whose status is 'active' and sort them by the field score in reverse order.

More information about querying with Mongoose

Assigning an API route to the new callback

At the bottom of the same file (server.api.js), let's locate the function getRoutes and declare a route+callback(s) for the function we just added.

Depending on the HTTP method we need, we will add it in its corresponding section (GET, POST, PUT, DELETE).

// API ROUTE LIST
exports.getRoutes = function () {
  return {
    get: {
      '/api/users': [ exports.listUsers ],    // <<<  Our new callback
      '/api/users/:id': [ exports.getUser ],
      '/api/events': [ exports.events ]
    },
    post: {
      '/api/users': [ checkLogin, exports.createUser ],
      '/api/upload': [ exports.upload ]
    },
    put: {
      '/api/users/:id': [ exports.updateUser ]
    },
    'delete': {
      '/api/users/:id': [ exports.removeUser ]
    }
  };
};

Now, when a request is made to /api/users the new function in server.api.js will handle the result.

Some functions may need to check various conditions before they serve the actual data (authentication status). That's why instead of assigning the path to a single callback, this block allows to define an (ordered) array of them.

This allows us to perform validation checks in earlier callbacks that will interrupt the request if something is wrong. For example, if we added a callback before handling the user creation:

post: {
      '/api/users': [ checkLogin, exports.createUser ],
      ...

checkLogin would handle the request before passing the control to exports.createUser. So the first function might look like that:

function checkLogin(req, res, next) {
  if (req.session.user) {
    next();
  } else {
    res.send({error: "You need to log in"});
  }
}

When an ExpressJS callback ends with next(), the next callback is executed (in this case exports.createUser). If a response is sent instead, no further processing is performed.

See this link for a complete ExpressJS session management example.

Frontend

API calls

To use the API we defined on the backend, first we need to edit www/scripts/api.js and create a new entry on the API factory. It must match the URL path we chose previously and include the necessary parameters (if any).

.factory('API', function($http) {
  return {
    listUsers: function() {                  // <<< THE NEW FUNCTION
		return $http.get("/api/users");      // <<< THE NEW API
    },
    getUser: function(id) {
		return $http.get("/api/users/" + id);
    },
    newUser: function(user) {
		return $http.post("/api/users", user);
    },
    updateUser: function(id, user) {
		return $http.put("/api/users/" + id, user);
    },
    deleteUser: function(id) {
		return $http.delete("/api/users/" + id);
    }
    // ...
  }
})

From now on, any Angular controller where the API service is injected, will allow us to invoke the new server call like this:

app.controller('ListCtrl', function($scope, API, DATA) {

	$scope.users = [];

	API.listUsers()
	.success(function(users, status, headers, config) {
		if(status != 200) {
			location.hash = "/";
			return;
		}
		if(typeof users == "object") {
			if(users.error) {
				alert("Error: " + users.error);
				return;
			}
			$scope.users = [];

			for(var i = 0; i < users.length; i++) {
				users[i].data = new Date(users[i].data);
				
				$scope.users.push(users[i]);
			}
			DATA.users = users;
		}
	})
	.error(function(object, status, headers, config) {
		// Network or server error
		alert("Unable to connect to the server");
	});
});

API.listUsers() is the new function we added on api.js and it returns a promise, not the actual value.

More information about promises.

When the promise is resolved, the function inside the success block will be executed. In case of a network error (unrelated to the application logic), the function inside the error block will be executed.

View templates

Once our data is in the $scope of a controller, we will create an html file on the www/views folder. For example user.html (could also be JADE).

<!-- USER LIST -->

<div id="users" class="row">

  <div class="col-lg-12">
    <div class="widget">
      <div class="widget-title">
        <i class="fa fa-users"></i> Active users
        <div class="clearfix"></div>
      </div>
      <div class="widget-body medium no-padding">
        <div class="table-responsive">
          <table class="table">
            <thead>
              <tr ng-if="users">
                <th>#</th>
                <th>Name</th>
                <th>Nick</th>
                <th>Score</th>
              </tr>
            </thead>
            <tbody>
              <loading ng-if="!users"></loading>

              <tr ng-repeat="user in users">
                <td><a ng-href="#/users/{{user.nick}}">{{($index+1)}}</a></td>
                <td><a ng-href="#/users/{{user.nick}}">{{user.name + ' ' + user.lastname}}</a></td>
                <td><a ng-href="#/users/{{user.nick}}">{{user.nick}}</a></td>
                <td><a ng-href="#/users/{{user.nick}}">{{user.score.toFixed(0)}}</a></td>
              </tr>
            </tbody>
          </table>
        </div>
      </div>
    </div>
  </div>

</div>

NOTE: You don't need to indicate the ng-controller on the markup. This is being handled in another file. (See below)

When the URL is http://hostname/#/users, we want that the new template is injected inside the following HTML tag in www/index.html.

<div ng-view autoscroll="true" class="view-slide-in"></div>

So let's edit www/scripts/index.js (at the top of it) to add an entry for that:

app.config(function($routeProvider) {
	$routeProvider
	.when('/', {
		templateUrl: 'views/splash.html',
		controller: 'SplashCtrl'
	})
	.when('/summary', {
		templateUrl: 'views/summary.html',
		controller: 'SummaryCtrl'
	})
	.when('/users', {                       // << THE NEW ENTRY
		templateUrl: 'views/users.html',
		controller: 'ListCtrl'
	})

When the hash is #/user Angular will load the new template and will pass its control to the ListCtrl controller we showed previously (so no need to specify an ng-controller in the markup).

Now we can navigate to any template we define by jumping to its corresponding route on the URL hash.

Sharing data among controllers

Content loaded from the server is stored in the $scope of the controller requesting them. However, if we leave the controller and jump to another one, this information is lost.

If we want to share content among controllers, we can use the DATA service (www/scripts/api.js), injecting it wherever we need to access global (or persistent) content.

app.controller('StartCtrl', function($scope, API, DATA) {

    DATA.users = [ {name: "Jordi"}, {name: "John"} ];
    DATA.persist();  // save the state to the localStorage
});

app.controller('MainCtrl', function($scope, API, DATA) {

    console.log(DATA.users);  // will print [ {name: "Jordi"}, {name: "John"} ]
});

First we assign any value from a controller. When we switch to another one, our values will stay and will remain accessible regardless of the $scope.

However, if we exit or reload the page, these contents will be lost. To keep them in memory we can call DATA.persist(). Now, the next controller injecting the DATA service, will have them restored automagically.

Other

WYSWYG Node friendly editors

About

Heroku Boilerplate

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published