Putting It All Together - BACK-END PROTOTYPING - Rapid Prototyping with JS: Agile JavaScript Development (2014)

Rapid Prototyping with JS: Agile JavaScript Development (2014)

III. BACK-END PROTOTYPING

7. Putting It All Together

Summary: descriptions of different deployment approaches, final version of Chat application and its deployment.

“Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it.”Brian W. Kernighan

Now, it would be good if we could put our front-end and back-end applications so they could work together. There are a few ways to do it:

· Different domains (Heroku apps) for front-end and back-end apps: make sure there are no cross-domain issues by using CORS or JSONP. This approach is covered in detail later.

· Same domain deployment: make sure Node.js process static resources and assets for front-end application — not recommended for serious production applications.

7.1 Different Domain Deployment

This is, so far, the best practice for the production environment. Back-end applications are usually deployed at the http://app. or http://api. subdomains.

One way to make a different domain deployment work is to overcome the same-domain limitation of AJAX technology with JSONP:

1 var request = $.ajax({

2 url: url,

3 dataType: "jsonp",

4 data: {...},

5 jsonpCallback: "fetchData",

6 type: "GET"

7 });

The other, and better, way to do it is to add the OPTIONS method, and special headers, which are called CORS, to the Node.js server app before the output:

1 ...

2 response.writeHead(200,{

3 'Access-Control-Allow-Origin': origin,

4 'Content-Type':'text/plain',

5 'Content-Length':body.length

6 });

7 ...

or

1 ...

2 res.writeHead(200, {

3 'Access-Control-Allow-Origin', 'your-domain-name',

4 ...

5 });

6 ...

The need for the OPTIONS method is outlined in HTTP access control (CORS). The OPTIONS request can be dealt with in the following manner:

1 ...

2 if (request.method=="OPTIONS") {

3 response.writeHead("204", "No Content", {

4 "Access-Control-Allow-Origin": origin,

5 "Access-Control-Allow-Methods":

6 "GET, POST, PUT, DELETE, OPTIONS",

7 "Access-Control-Allow-Headers": "content-type, accept",

8 "Access-Control-Max-Age": 10, // Seconds.

9 "Content-Length": 0

10 });

11 response.end();

12 };

13 ...

7.2 Changing Endpoints

Our front-end application used Parse.com as a replacement for a back-end application. Now we can switch to our own back-end replacing the endpoints (yes, it’s that painless!). The front-end app source code is in the rpjs/board GitHub folder.

In the beginning of the app.js file, uncomment the first line for running locally, or replace the URL values with your Heroku or Windows Azure back-end application public URLs:

1 // var URL = "http://localhost:5000/";

2 var URL ="http://your-app-name.herokuapp.com/";

As you can see, most of the code in app.js and the folder structure remained intact with the exception of replacing Parse.com models and collections with original Backbone.js ones:

1 Message = Backbone.Model.extend({

2 url: URL + "messages/create.json"

3 })

4 MessageBoard = Backbone.Collection.extend ({

5 model: Message,

6 url: URL + "messages/list.json"

7 });

Those are the places where Backbone.js looks up for REST API URLs corresponding to the specific collection and model.

Here is the full source code of the rpjs/board/app.js file:

1 /*

2 Rapid Prototyping with JS is a JavaScript

3 and Node.js book that will teach you how to

4 build mobile and web apps fast. — Read more at

5 http://rapidprototypingwithjs.com.

6 */

7

8 // var URL = "http://localhost:5000/";

9 var URL = "http://your-app-name.herokuapp.com/";

10

11 require([

12 'libs/text!header.html',

13 'libs/text!home.html',

14 'libs/text!footer.html'],

15

16 function(

17 headerTpl,

18 homeTpl,

19 footerTpl) {

20

21 var ApplicationRouter = Backbone.Router.extend({

22 routes: {

23 "": "home",

24 "*actions": "home"

25 },

26 initialize: function() {

27 this.headerView = new HeaderView();

28 this.headerView.render();

29 this.footerView = new FooterView();

30 this.footerView.render();

31 },

32 home: function() {

33 this.homeView = new HomeView();

34 this.homeView.render();

35 }

36 });

37

38 HeaderView = Backbone.View.extend({

39 el: "#header",

40 templateFileName: "header.html",

41 template: headerTpl,

42 initialize: function() {},

43 render: function() {

44 $(this.el).html(_.template(this.template));

45 }

46 });

47

48 FooterView = Backbone.View.extend({

49 el: "#footer",

50 template: footerTpl,

51 render: function() {

52 this.$el.html(_.template(this.template));

53 }

54 });

55 Message = Backbone.Model.extend({

56 url: URL + "messages/create.json"

57 })

58 MessageBoard = Backbone.Collection.extend({

59 model: Message,

60 url: URL + "messages/list.json"

61 });

62

63 HomeView = Backbone.View.extend({

64 el: "#content",

65 template: homeTpl,

66 events: {

67 "click #send": "saveMessage"

68 },

69

70 initialize: function() {

71 this.collection = new MessageBoard();

72 this.collection.bind("all", this.render, this);

73 this.collection.fetch();

74 this.collection.on("add", function(message) {

75 message.save(null, {

76 success: function(message) {

77 console.log('saved ' + message);

78 },

79 error: function(message) {

80 console.log('error');

81 }

82 });

83 console.log('saved' + message);

84 })

85 },

86 saveMessage: function() {

87 var newMessageForm = $("#new-message");

88 var username = newMessageForm.find('[name="username"]')

89 .attr('value');

90 var message = newMessageForm.find('[name="message"]')

91 .attr('value');

92 this.collection.add({

93 "username": username,

94 "message": message

95 });

96 },

97 render: function() {

98 console.log(this.collection)

99 $(this.el).html(_.template(

100 this.template,

101 this.collection

102 ));

103 }

104 });

105

106 app = new ApplicationRouter();

107 Backbone.history.start();

108 });

7.3 Chat Application

The back-end Node.js application source code is in the rpjs/node GitHub folder, which has this structure:

1 /node

2 -web.js

3 -Procfile

4 -package.json

Here is a source code of web.js, our Node.js application implemented with CORS headers:

1 /*

2 Rapid Prototyping with JS is a JavaScript

3 and Node.js book that will teach you how to build mobile

4 and web apps fast. — Read more at

5 http://rapidprototypingwithjs.com.

6 */

7

8 var http = require('http');

9 var util = require('util');

10 var querystring = require('querystring');

11 var mongo = require('mongodb');

12

13 var host = process.env.MONGOHQ_URL ||

14 "mongodb://localhost:27017/board";

15 //MONGOHQ_URL=mongodb://user:pass@server.mongohq.com/db_name

16

17

18 mongo.Db.connect(host, function(error, client) {

19 if (error) throw error;

20 var collection = new mongo.Collection(client, 'messages');

21 var app = http.createServer( function (request, response) {

22 var origin = (request.headers.origin || "*");

23 if (request.method=="OPTIONS") {

24 response.writeHead("204", "No Content", {

25 "Access-Control-Allow-Origin": origin,

26 "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE,

27 OPTIONS",

28 "Access-Control-Allow-Headers": "content-type, accept",

29 "Access-Control-Max-Age": 10, // Seconds.

30 "Content-Length": 0

31 });

32 response.end();

33 };

34 if (request.method==="GET"&&

35 request.url==="/messages/list.json") {

36 collection.find().toArray(function(error,results) {

37 var body = JSON.stringify(results);

38 response.writeHead(200,{

39 'Access-Control-Allow-Origin': origin,

40 'Content-Type':'text/plain',

41 'Content-Length':body.length

42 });

43 console.log("LIST OF OBJECTS: ");

44 console.dir(results);

45 response.end(body);

46 });

47 };

48 if (request.method === "POST" &&

49 request.url === "/messages/create.json") {

50 request.on('data', function(data) {

51 console.log("RECEIVED DATA:")

52 console.log(data.toString('utf-8'));

53 collection.insert(JSON.parse(data.toString('utf-8')),

54 {safe:true}, function(error, obj) {

55 if (error) throw error;

56 console.log("OBJECT IS SAVED: ")

57 console.log(JSON.stringify(obj))

58 var body = JSON.stringify(obj);

59 response.writeHead(200,{

60 'Access-Control-Allow-Origin': origin,

61 'Content-Type':'text/plain',

62 'Content-Length':body.length

63 });

64 response.end(body);

65 })

66 })

67

68 };

69

70 });

71 var port = process.env.PORT || 5000;

72 app.listen(port);

73 })

7.4 Deployment

For your convenience, we have the front-end app in the rpjs/board folder and the back-end app with CORS under rpjs/node. By now, you probably know what to do, but as a reference, below are the steps to deploy these examples to Heroku.

In the node folder execute:

1 $ git init

2 $ git add .

3 $ git commit -am "first commit"

4 $ heroku create

5 $ heroku addons:add mongohq:sandbox

6 $ git push heroku master

Copy the URL and paste it into the board/app.js file, assigning the value to the URL variable. Then, in board folder, execute:

1 $ git init

2 $ git add .

3 $ git commit -am "first commit"

4 $ heroku create

5 $ git push heroku master

6 $ heroku open

7.5 Same Domain Deployment

Same domain deployment is not recommended for serious production applications, because static assets are better served with web servers like nginx (not Node.js I/O engine), and separating API makes for less complicated testing, increased robustness, and quicker troubleshooting/monitoring. However, the same app/domain approach could be used for staging, testing, development environments and/or tiny apps.

This is an example of a static Node.js server:

1 var http = require("http"),

2 url = require("url"),

3 path = require("path"),

4 fs = require("fs")

5 port = process.argv[2] || 8888;

6

7 http.createServer(function(request, response) {

8

9 var uri = url.parse(request.url).pathname

10 , filename = path.join(process.cwd(), uri);

11

12 path.exists(filename, function(exists) {

13 if(!exists) {

14 response.writeHead(404, {

15 "Content-Type": "text/plain"});

16 response.write("404 Not Found\n");

17 response.end();

18 return;

19 }

20

21 if (fs.statSync(filename).isDirectory())

22 filename += '/index.html';

23

24 fs.readFile(filename, "binary",

25 function(err, file) {

26 if(err) {

27 response.writeHead(500,

28 {"Content-Type": "text/plain"});

29 response.write(err + "\n");

30 response.end();

31 return;

32 }

33 response.writeHead(200);

34 response.write(file, "binary");

35 response.end();

36 }

37 );

38 });

39 }).listen(parseInt(port, 10));

40

41 console.log("Static file server running at\n "+

42 " => http://localhost:" + port + "/\nCTRL + C to shutdown");

information

Note

Another, more elegant way is to use Node.js frameworks as Connect (http://www.senchalabs.org/connect/static.html), or Express (http://expressjs.com/guide.html); because there is a special static middleware for JS and CSS assets.