Elvenware

NodeRouteBasics

Welcome to NodeRouteBasics

Overview

STATUS: This assignment needs polishing but should be complete enough to allow students to complete the assignment.

The Node Route Basics assignment gives you practice creating NodeJs Express routes and calling them with fetch. There is still at least one reference to $.getJSON, but by and large, I have tried to strip jQuery code out of the assignment.

If you need help with this this assignment, study the NodeRoutes examples in JsObjects.

The HTTP Protocol

We are sending a message from the client to the server, and then getting a response:

http

The client (web browser) uses HTTP to make a request for HTML, CSS, JavaScript or an image. The request might be triggered when we click on a link, type in the address bar or call an ajax function such as fetch or jQuery's getJSON.

TCP/IP is used to send the request via the network to the server. The server, which in our case is a NodeJs express web server, reads the HTTP request and we create a custom route in routes/index.js that sends a response back. The response is typically an HTML file, some JSON, or some other artifact sent via the HTTP protocol.

On the client, the browser unpacks the request. If it an HTML file the user requested, it parses the HTML, and displays the results to the user. If it is an ajax request, then we typically parse the JSON and display the result to the user in some HTML element.

Step One: Pull Elven Assignments

I have renamed the Prog270-Assignments repository to elven-assignments. If you don't have the repo, then do this:

git clone http://github.com/charliecalvert/elven-assignments.git

Step Two: Copy Project

Copy the node express program called NodeRouteBasics from my repo to yours:

cp -r ~/Git/elven-assignments/NodeRouteBasics ~/Git/prog272-lastname-2016/Week04-NodeRouteBasics

Step Three: Client Interface

It should include the following:

You interface will probably consist of three buttons:

Node Route Basics UI

Step ThreeA: Buttons, Jade and Clicks

Put this in views/index.jade or views/index.pug:

button#search Search

NOTE: Jade has been renamed to Pug. At this stage, it doesn't matter whether we are using Jade (index.jade) or Pug (index.pug). Though both behave the same way in nearly all cases.

To detect a click on this button, write something like this:

function search() {
    // YOUR CODE HERE
};

document.getElementById('search').onclick = search;

Step Four: Server

All the calculations should be performed on the server side, in a module, per the NodeRoutes02 example in JsObjects.

The return values should be a simple JavaScript literal (JSON) that contains at minimum, a property called result that contains the result of the calculation. For instance, our getNine method would set result to the number 9: {result: 9}. Like this:

router.get('/getNine', function(request, response) {
    'use strict';  
    response.send({"result": 9});
});

Call Server without Parameters

First, an example showing how to call a route (endpoint) without parameters:

function callServerWithoutParms() {

  fetch('/search')
    .then((response) => response.json())
    .then((response) => {
        const displayArea = document.getElementById('displayArea');
        displayArea.innerHTML = JSON.stringify(response, null, 4);
     })
    .catch((ex) => {
       console.log(ex);
     });
}

Call Route (endpoint) with Parameters

Sometimes we need to not just call a route on the server, but call the route and also pass in parameters. Suppose we want to calculate the number of feet in X miles, which X is supplied by the user in a text INPUT or numeric INPUT control.

First, define the input control in our Jade/Pug file:

extends layout

block content
  h1= title
  p Welcome to #{title}

  input#userInput(type="number")  <=== HERE

  div    
    button#calculateFeetFromMiles Calculate Feet from Miles    

  div
    pre#displayArea

Here is the client side code that calls that module:

const userMiles = document.getElementById('userInput').value;
  fetch('/calculateFeetFromMiles' + '?miles=' + userMiles)
      .then((response) => response.json())
      .then((response) => {
          const displayArea = document.getElementById('displayArea');
          displayArea.innerHTML = JSON.stringify(response, null, 4);
      })
      .catch(ex => {
          console.log(ex);
      });

Server Side HTTP GET Parameters

Define server side code that accepts a parameter:

router.get('/calculateFeetFromMiles', function(request, response) {
    response.send({result: request.query.miles * 5280});
});

The request (req) parameter has a property called query. Use it to access the parameters you passed to the server: request.query.miles.

Server Side HTTP POST Parameters

When you POST data to the server you need to pass in a JavaScript object literal as a second parameter to fetch. This second parameter is used to specify the options for your call. For instance, you can specify whether you want to make a GET or a POST call. By default, fetch uses GET. There are a number of possible options, but in many cases you will use only these three:

I find it a bit of a struggle to define the exact format of these options, so I have wrapped them in a little function called get getPostOptions:

function getPostOptions(body) {
    return {
        method: 'POST',
        headers: new Headers({
            'Content-Type': 'application/json'
        }),
        body: JSON.stringify(body)
    };
}

We call this function, passing in the parameters we want to pass to the server endpoint. If we wanted to pass in to parameters of type of string called param01 and param02, then we might call getPostOptions like this:

getPostOptions({
  param01: 'foo',
  param02: 'bar'
})

When we call fetch usually just pass in one parameter:

fetch('/some-url')
  .then etc...

When POSTing data, however, we should pass in two parameters. The first is our URL, and the second the options returned from our utility function:

fetch('/some-url', getPostOptions({...}));

Here is a more complete example of the type of call you can use to complete calculateCircumference portion of this assignment:

function callServer() {
    const userInput = document.getElementById('userInput').value;
    const query = {propForServer: userInput};

    fetch('/some-url', getPostOptions(query))
        .then((response) => response.json())
        .then((response) => {
            const displayArea = document.getElementById('displayArea');
            displayArea.innerHTML = JSON.stringify(response, null, 4);
        })
        .catch((ex) => {
            console.log(ex);
        });
}

On the server side, everything looks the same except that we use router.post rather than router.get and we use request.body rather than request.query:

router.post('/calculateCircumference', function(request, response) {
    console.log(request.body);
    // YOU WRITE THE CODE TO SEND BACK THE RESPONSE
});

Extra Credit

For three points extra credit, implement getFeetInMile and calculateFeetFromMiles using HTTP GET calls, and use POST for calculateCircumference:

var express = require('express');
var router = express.Router();

router.get(...)
router.post(...)

If you are going for extra-credit, please add a note to that effect when you submit the assignment.

The formula for calculating the circumference of a circle given its radius looks like this:

const    circumference = 2 * radius * Math.PI;

The parameter for calculateFeetFromMiles: miles

The parameter for calculateCircumference: radius

Recall that with GET methods we use frequently use request.query to find parameters, but with POST methods we use request.body.

Step Five

Put a calculateCircumference method in a file called routes/utils.js. In that file create a simple object literal:

module.exports = {
    // YOUR METHOD HERE
}

Now require your utils.js file in routes/index.js and use it in the appropriate route on your server. The method should take one parameter called radius and it should return the calculated circumference.

NOTE: If we are building our own NPM packages, then put this object and method in the package instead. Otherwise just use the technique outlined above. In either case, our goal is to learn how to create reusable code that we can plug into an project on the server side.

Turn It In

Check your code into your Git repository and submit the URL of your repository or of the project you submitted.

LastPass

I use LastPass. It puts an icon in many input controls. To turn that off, add an attribute to your input controls called data_lpignore:

For instance, in your Pug file do this:

input#userInput(type="number", data_lpignore="true")

To turn it off for all INPUT controls on your page, add this near the top of control.js:

const elements = document.getElementsByTagName("INPUT");
for (let element of elements) {
    element.setAttribute("data_lpignore", "true");
}

To turn it off in a specific control, one could write something like this:

document.getElementById('userInput').setAttribute("data_lpignore", "true");****

Or, you can do it by class name:

const elements = document.getElementsByClassName('no-last-pass');
for (let element of elements) {
    element.setAttribute("data_lpignore", "true");
}

More Complex Example

This does not directly relate to the current assignment, but I'm leaving it here for now. It still has jQuery code in it.

Now an example showing how to call a route with parameters:

function callServerWithParms() {

    // Get Data We Want to Pass to the Server
    var dirsToWalk = document.getElementById('dirsToWalk');
    var directory = dirsToWalk.options[sourceIndex].value;
    var destinationDirs = document.getElementById('destinationDirs');
    var destinationDir = destinationDirs.options[destinationIndex].value;

    var highlight = $('#highlight').prop('checked');

    // Put that data in JavaScript Object
    var requestQuery = {
        directoryToWalk: directory,
        destinationDir: destinationDir,
        highlight: highlight,
    };

    // Call the server and pass the data as a parameter.
    $.getJSON('/walk', requestQuery, function (result) {
        elf.display.showApacheFiles(result.htmlFilesWritten, result.destinationDir);
        elf.display.fillDisplayArea(JSON.stringify(result, null, 4));
    }).done(function () {
            elf.display.showDebug('Walk loaded second success');
        })
        .fail(function (jqxhr, textStatus, error) {
            elf.display.showDebug('Walk loaded error: ' + jqxhr.status + ' ' + textStatus + ' ' + error);
        })
        .always(function () {
            elf.display.showDebug('Walk loaded complete');
        });

}

Below, in the next section, we look more closely at passing parameters.

Client Server Interactions

Look at these code excerpts from the code shown above. We look specifically at the call to the server:


    // Put that data in JavaScript Object
    var requestQuery = {
        directoryToWalk: directory,
        destinationDir: destinationDir,
        highlight: highlight,
    };

    // Call the server and pass the data as a parameter.
    $.getJSON('/walk', requestQuery, function (result) { ... });

And here is what it looks like on the server. Notice how we use the request object to discover the parameters passed by the client:


router.get('/walk', function(request, response) {
    'use strict';
    console.log('In walk', request.query);
    var directoryToWalk = request.query.directoryToWalk;
    var destinationDir = request.query.destinationDir;
    var highlight = request.query.highlight;
    etc
});

Update Out of Date Packages

$ npm install -g npm-check-updates
$ npm-check-updates -u
$ npm install

If it is still not up to date, do: ncu -a

Also, be sure to remove phantomjs from packages.json.

See here:

Setting up the Port

Let's allways use the following:

var port = process.env.PORT || 30025;

// Code omitted here...

app.listen(port);
console.log('Listening on port :' + port);  

We want to pick a particular port because in some situations, such as running on EC2, we need to open the port ahead of time. By choosing one port, and always using it, you won't have to edit my code before you can run it, and vice versa.

Node Express Routing Basics

Express offers support for HTTP verbs such as Get, Post, Put, etc.

The verbs provide a response to specific routes, such as '/':

app.get('/', function(req, res) {
    console.log("root request sent");
});

Or here is request that uses a wildcards or regular expressions:

app.get('/a*', function(req, res) {
    console.log("A request sent that begins with an a");
});

Working with numbers:

app.get('/book/:id((d+)', function(req, res) {
    console.log("Only requests that are numbers");
});

Node Express Serving up Static Pages

Put your static files in a particular directory and tell express about the directory:

app.use("/public", express.static(__dirname + '/public'));

Server them up like this:

app.get('/', function(req, res) {
    var html = fs.readFileSync('public/index.html');
    res.writeHeader(200, {"Content-Type": "text/html"});
    res.end(html);
});

Temp directory

This is optional. Skip if you are not interested.

Instead of running npm install and bower install, do this:

For instance

mkdir ~/tmp
cp ~/Git/isit322-lastname-2016/Week04-Middleware/node_modules ~/tmp
cp ~/Git/isit322-lastname-2016/Week04-Middleware/public/components ~/tmp

You might want to also copy the package.json and bower.json files to tmp. As needed, update your files to the latest:

cd ~/tmp
npm outdated --depth=0

And then get the latest of everything as needed.

Now go back to your project and create symbolic links to these packages. The best way to do this is to use

Just as an fyi, here they are:

alias run="nm && components && npm start"
alias nm="ln -s ~/tmp/node_modules/"
alias components="ln -s ~/tmp/components/ public/components"

For this project, do the following in ~/tmp, or, if you have completed the above, in your project:

npm install supertest --save-dev
npm install jasmine --save-dev
npm install -g jasmine

NOTE: The point of setting up this ~/tmp directory is to put an end to long npm installs during class. Talk to Adam, he knows all about this.