Representational State Transfer (REST) at Front-End (AngularJS) and Back-End

When I started the myLibrary project, I invest some of my time to try and analyze a few choices out there on Internet to put on the Front-End and Back-End. Then AngularJS, an excellent choice as Front-End and ExpressJS working as Back-End.

But down the road, I found that either AngularJS and ExpressJS offer REST functionality. So, which one to use to talk to MongoDB? I could have used only AngularJS with its $http service to get data from the database, but I have decided to maintain independent the Front-End of Back-End. Development and experience aspects!

To do so, I do some research to know how the REST works in the ExpressJS and AngularJS worlds. One day, I was wondering how connect two REST services? Because both software do the same process: resources for AngularJS and routes for ExpressJS.

Front-End

First implementation, the routes file contains some of possible routes that can be accessed in the client’s browser:

  • /documents: show all documents.
  • /view/:id: view a specific document passing its ID as parameter.
  • /subjects/:subject: show all books under the specific subject, passed as parameter.

This can be possible because the AngularJS UI router module is implemented as follows:

'use strict';

/**
 * 
 * @name myLibraryApp
 * @description
 * # myLibraryApp
 *
 * Main module of the application.
 */
angular.module('myLibraryApp')
	.config(function ($stateProvider, $urlRouterProvider){
		$stateProvider

...

			/*
			 * DOCUMENTS
			 */
			.state('documents', {
				url: '/documents',
				templateUrl: '/documents/document.html'
			})
			// creates the URL of each book on the search results list with ui-sref
			.state('documents.detail', {
				url:'/view/:id',
				views:{
					'' : {
						templateUrl: '/documents/document.detail.html',
						controller: 'BookInfoController'
					},
					'viewSubjectsNavBar' : {
						templateUrl: '/subjects/subjects.NavBar.html',
						controller: 'SubjectsListController'
					}
				}
			})
			.state('documents.subject', {
				url: '/subjects/:subject',
				views : {
					'' : {
						templateUrl: '/documents/document.list.html',
						controller: 'DocumentListController'
					},
					'viewSubjectsNavBar' : {
						templateUrl: '/subjects/subjects.NavBar.html',
						controller: 'SubjectsListController'
					}
				}
			})

...

		// For any unmatched url, redirect to /
		$urlRouterProvider.otherwise('/');
	});
 

Second implementation, the services file contains factories that create $resources to interact with data sources in the server-side via HTTP protocol.

Here is an example of a Documents resource that is pointing to our Back-End service to interact with the documents stored into the database. Documents because we use MongoDB.

Note that we can pass the id parameter in the URL. This is used to either get a specific document, update it or delete it. Of course, through the correct CRUD method. The URL can be any thing else, depend on your needs. In this case, we run the app in the same computer, so that’s why localhost and because ExpressJS uses the port 3000 (also editable).

'use strict';

/**
 * 
 * @name myLibraryApp.services
 * @description
 * # myLibraryApp.services
 *
 * Main service module of the application.
 */
angular.module('myLibraryApp.services', [])
	.factory('Documents', function( $resource ) {
		return $resource('http://localhost:3000/documents/:id', { id : '@_id' }, {
			'query' : { method : 'GET', params : { subject : '@subject' }, isArray : true },
			'update' : { method : 'PUT' }
		});
	});
 

Back-End

In order to separate the Client of Server side, between the good proposed systems, ExpressJS was chosen because at the time I was planning the myLibrary project, it was a good choice for the project. This was before I found LoopBack.

Possible runners:

  1. .NET
  2. Java
  3. Express (current implementation)
  4. LoopBack (to-implement project)

So, in ExpressJS app configuration file, we create a variable to store the documents route. We define the custom URL to interact with the database. Note that documents parameter is the variable previously declared.

...

var documents = require('./routes/documents');

...

var app = express();

...

app.use('/', routes);
app.use('/users', users);
app.use('/subjects', subjects);
app.use('/documents', documents);

...

module.exports = app;
 

Now, in the documents route we can see how the different CRUD methods are implemented.

The document route has the following methods:

  1. router.get(‘/’, function(req, res): returns all documents.
  2. router.get(‘/count’, function(req, res): returns subjects list with the total of books.
  3. router.get(‘/subject/:subject’, function(req, res): returns all books under the specific subject.
  4. router.get(‘/view/:id’, function(req, res): returns data of a specific book where its ID corresponds to the parameter ID.
  5. router.put(‘/:id’, function(req, res, next): updates a specific book where its ID corresponds to the parameter ID.
  6. router.post(‘/’, function (req, res, next): creates a new book on the database. This is used when we submit the new book form.
  7. router.delete(‘/:id’, function(req, res, next): deletes a specific book where its ID corresponds to the parameter ID.
var express = require('express');
var router = express.Router();

// mongoDB
var mongoose = require('mongoose');

// connection status
console.log('MongoDB connection status: '+ mongoose.connection.readyState);

// Connect to DB
if (mongoose.connection.readyState == 0){
	console.log('Connecting...');

	mongoose.connect('mongodb://localhost/pubLibrary', function(err) {
		if(err) {
			console.log('Connection to MongoDB Server error', err);
		} else {
			console.log('Connection successful to MongoDB Server\n');
		}
	});
};

var schemas = require('../bin/schemas.js');

//New subject model
var documents = mongoose.model('documents', schemas.documents);

// normal root
router.get('/', function(req, res){
	// Start timing now
	console.time('documents.router.get.job.timespan');

	var results = null;

	if (req.query.id != undefined && req.query.id != ' '){
		documents.findById(req.query.id, function(err, docs){
			if(err) res.json(err);
			else res.json(docs);
		});
	}else if (req.query.subject != undefined && req.query.subject != ' '){
		documents.find({ subject : req.query.subject }, function(err, docs){
			if(err) res.json(err);
			else res.json(docs);
		});
	}else{
		// return all documents
		documents.find({}, function(err, docs){
			if(err) res.json(err);
			else res.json(docs);
		});
	}

	// log
	console.log('documents.router.get');
	console.log('query: '+ JSON.stringify(req.query));
	console.log('params: '+ JSON.stringify(req.params));

	// Stop timer.
	console.timeEnd('documents.router.get.job.timespan');
	console.log('\n');
});

// count
router.get('/count', function(req, res){
	// Start timing now
	console.time('documents.router.get.count.job.timespan');

	//get all documents on collection
	documents.aggregate([
		{ 
			$group : {
				_id : '$subject'
				,count : { $sum : 1 }
			}
		}
		,{ 
			$sort : {
				_id : 1 
			} 
		}]
		,function(err, docs){
			if(err) res.json(err);
			else {
				// return results
				res.json(docs);

				// log
				console.log('documents.router.get.count');
				console.log('query: '+ JSON.stringify(req.query));
				console.log('params: '+ JSON.stringify(req.params));

				// Stop timer.
				console.timeEnd('documents.router.get.count.job.timespan');
				console.log('\n');
			}
	});
});

// view all subject's books
router.get('/subject/:subject', function(req, res){
	// Start timing now
	console.time('documents.router.get.subject.job.timespan');

	//get all documents on collection
	documents.find({ subject : req.params.subject }, function(err, docs){
		if(err) res.json(err)
		else {
			//return results
			res.json(docs);

			// log
			console.log('documents.router.get.subject');
			console.log('query: '+ JSON.stringify(req.query));
			console.log('params: '+ JSON.stringify(req.params));

			// Stop timer.
			console.timeEnd('documents.router.get.subject.job.timespan');
			console.log('\n');
		}
	});
});

// view one document by its ID
router.get('/view/:id', function(req, res){
	// Start timing now
	console.time('documents.router.get.viewID.job.timespan');

	//get document by id
	documents.findById(req.params.id, function(err, docs){
		if(err) res.json(err);
		else {
			// return results
			res.json(docs);

			// log
			console.log('documents.router.get.viewID');
			console.log('query: '+ JSON.stringify(req.query));
			console.log('params: '+ JSON.stringify(req.params));

			// Stop timer.
			console.timeEnd('documents.router.get.viewID.job.timespan');
			console.log('\n');
		}
	});
});


// edit/update document on DB
router.put('/:id', function(req, res, next){
	// Start timing now
	console.time('documents.router.put.id.job.timespan');

	//delete request data item _id
	delete req.body._id;

	//update document on db
	documents.findByIdAndUpdate(req.params.id, req.body, function(err, post){
		if(err) return next(err);
		else{
			//return results
			res.json(post);

			// log
			console.log('documents.router.put.id');
			console.log('id: '+ req.params.id);
			console.log('post: '+ JSON.stringify(post));
			console.log('query: '+ JSON.stringify(req.query));
			console.log('params: '+ JSON.stringify(req.params));
			console.log('body: '+ JSON.stringify(req.body));

			// Stop timer.
			console.timeEnd('documents.router.put.id.job.timespan');
			console.log('\n');
		}
	});
});

// new document
router.post('/', function (req, res, next){
	// Start timing now
	console.time("documents.router.post.job.timespan");

	// create new document document into the collection
	documents.create(req.body, function(err, post){
		if(err) return next(err);
		else {
			//return results
			res.json(post);

			// log
			console.log('documents.router.post');
			console.log('id: '+ req.params.id);
			console.log('post: '+ JSON.stringify(post));
			console.log('query: '+ JSON.stringify(req.query));
			console.log('params: '+ JSON.stringify(req.params));
			console.log('body: '+ JSON.stringify(req.body));

			// Stop timer.
			console.timeEnd('documents.router.post.job.timespan');
			console.log('\n');
		}
	});
});

// delete this document by its ID
router.delete('/:id', function(req, res, next){
	// Start timing now
	console.time("documents.router.delete.id.job.timespan");

	//remove document
	documents.findByIdAndRemove(req.params.id, req.body, function(err, post){
		if(err) return next(err);
		else {
			//return results
			res.json(post);

			// log
			console.log('documents.router.delete.id');
			console.log('id: '+ req.params.id);
			console.log('post: '+ JSON.stringify(post));
			console.log('query: '+ JSON.stringify(req.query));
			console.log('params: '+ JSON.stringify(req.params));
			console.log('body: '+ JSON.stringify(req.body));

			// Stop timer.
			console.timeEnd('documents.router.delete.id.job.timespan');
			console.log('\n');
		}
	});
});

module.exports = router;
 

Conclusion

After all, this is a way of calling sources either a page in the app (some file located in another place) or interact with data sources or replace file names in the URL and render human readable the URL.

Implementing this software architecture, we surely will have this advantages (they can be more):

  1. Performance
  2. Scability
  3. Simplicity of interfaces
  4. Portability

Related Links

http://www.mongodb.com/blog/post/building-your-first-application-mongodb-creating-rest-api-using-mean-stack-part-1

https://docs.angularjs.org/tutorial/step_11

https://docs.angularjs.org/api/ngResource/service/$resource

https://jersey.java.net/

http://www.asp.net/web-api/overview/web-api-routing-and-actions

http://expressjs.com/guide/routing.html

http://loopback.io/

Leave a comment