Skip to content

Instantly share code, notes, and snippets.

@MrJadaml
Last active March 28, 2017 21:23
Show Gist options
  • Select an option

  • Save MrJadaml/092319a794f7f33f668f36775a47328f to your computer and use it in GitHub Desktop.

Select an option

Save MrJadaml/092319a794f7f33f668f36775a47328f to your computer and use it in GitHub Desktop.

HTTP and Node.js

Objectives

By the end of this lesson, you should be able to...

  • Explain what an HTTP server is
  • Explain why an HTTP server is useful
  • Explain what an HTTP request is
  • Explain what an HTTP response is
  • Create a Node.js HTTP server with the http module
  • Identify the two parts of a route (method and path)

What's a Web Server?

Node.js is commonly used to build HTTP servers. An HTTP server is a program that runs in an infinite loop, accepting HTTP requests from a client and sending HTTP responses back to it. Inside those responses, HTTP servers often include data like HTML, CSS, JavaScript, and JSON among other formats.

See this Web sequence diagram going to google for how it all works together.

Why is an HTTP server useful?

In 1989, Tim Berners-Lee proposed a new project to his employer CERN. The goal of this project was to ease the exchange of information between scientists by using a hypertext system. In 1990, the project resulted in two programs—the world's first HTTP client and server.

Because exchanging information over HTTP is so effective, there are countless HTTP clients and servers in existence today. The most effective of these servers are able to handle thousands of requests per second, thus giving millions of people around the world the ability to access and exchange information every day. Without HTTP servers, the rapid and global exchange of information over the Internet would not exist.

Previously, Node.js has been used as a mechanism for interacting with the filesystem with Node.js' fs module. Before beginning this lesson, make sure you can identify at least 2 reasons why fs is an important module in the Node.js ecosystem.

Create an HTTP server with Node.js

Create a server.js file on the Desktop.

take ~/playground
touch server.js

Open the server.js file in your text editor.

atom .

And type in the following code.

const http = require('http');

const server = http.createServer(function(request, response) {
  response.setHeader('Content-Type', 'text/plain');
  response.end('Hello world');
});

server.listen(8000, function() {
  console.log('Listening at localhost:8000');
});

Run the server with the node command.

node server.js

Now, this server will listen for HTTP Requests and then respond with an HTTP Response. We can test this several ways:

Sending HTTP Requests with httpie

In a new Terminal tab, send an HTTP request to the server.

http GET localhost:8000/

And you should see something like this.

Sending HTTP Requests with a Browser

The simplest and most familiar way to send an HTTP request is to use what you're used to creating them with, which is your browser. Visiting localhost:8000 in a browser will send a request to the URL you put in the URL bar.

What's an HTTP Request?

The client (or user agent) sends a plain-text message called an HTTP request to a server on behalf of the user. Aside from web browsers, other common user agents include web crawlers, native apps, and mobile apps.

An HTTP request is composed of the following parts.

  1. A method (or verb)
  2. A path
  3. An HTTP version
  4. Key-value headers
  5. And an optional body

Here's an example of what an HTTP request looks like.

GET / HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Host: localhost:8000
User-Agent: HTTPie/0.9.3

QUESTION: Looking at the above message, can you identify the parts of an HTTP request?

While an HTTP request can only contain one method, there are several different methods that a client can choose from. Each method instructs a server on how to process the request. Without the use of Ajax, web browsers are only capable of sending HTTP requests with the following methods.

Method Description
GET Used retrieve a resource, like an HTML file, from a server.
POST Used send information, like user input, to a server.

You'll learn about additional HTTP methods, like PUT, PATCH, and DELETE, when we encounter RESTful HTTP servers later in the course.

What's an HTTP response?

When the server receives an HTTP request, its job is to process the request and respond to the client with a plain-text message. In addition to Node.js, popular HTTP servers include Apache, Nginx, and Python's built-in SimpleHTTPServer.

An HTTP response is composed of the following parts.

  1. HTTP version
  2. Status code
  3. Key-value headers
  4. Optional body

Here's an example of what an HTTP response looks like.

HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 11
Content-Type: text/plain
Date: Mon, 13 Jun 2016 04:28:36 GMT

Hello world

QUESTION: Looking at the above message, can you identify the parts of an HTTP response?

While an HTTP response can only contain one status code, there are many different codes that a server can choose from. Each status code explains to the client how the server interpreted the request. Status codes are three-digit numbers that are grouped into the following categories.

Status Code Group Description
1XX Request accepted, ready for the next one.
2XX Request accepted, the server's work is complete.
3XX Request accepted, but additional client work is needed.
4XX Request accepted, but there was an error on the client.
5XX Request accepted, but there was an error on the server.

QUESTION: The most common status codes are 200, 302, 304, 404, and 500. Can you figure out why?

Walkthrough : A Guest List

Update the request handler for your HTTP server:

const http = require('http');

const server = http.createServer(function(request, response) {
  let guests = ['Mary', 'Don'];
  let guestsJSON = JSON.stringify(guests);

  response.setHeader('Content-Type', 'application/json');
  response.end(guestsJSON);
});

server.listen(8000, function() {
  console.log('Listening at localhost:8000');
});

Server breakdown - how it's configured:

  • We require the http module, which is included as part of Node's standard library.
  • We create a server using the createServer method of http
  • We are also passing the server a callback function to handle any requests made to our server.
  • We tell our server to listen for any incoming HTTP Requests on port 8000.

request handler

function signature

The callback function takes two arguments - request and response. Typically you'll see these abbrivieted as req and res and how we'll be using them going forward. The callback's request argument will contain the incoming HTTP request object. The callback's response argument will contain an empty outgoing HTTP response object. The goal of the callback is to correctly build out the response object based on the information in request object.

function body

  • We create an ordinary array, nothing special
  • We turn the array into a string. The only thing we can really send via HTTP is a string, so we convert our data into strings before we send it.
  • We configure the data we need to send back to whomever sent us the request. Really, all we're doing is building up the string of text that you saw in the "What is an HTTP Response?" section. Using the setHeader method adds a key (the name of the header) and value to the response we'll be sending out.
  • The end method is our server's way of saying "ship it!" - it sends the response and tells the client "there won't be more stuff coming after this".

Now, save the server.js file and run it with the node command.

node server.js

And you should see something like this.

In a separate Terminal tab, send the following HTTP request to the server.

http GET localhost:8000/

And you should see something like this.

Json res

Routes

Right now, your HTTP server handles every HTTP request the same way, regardless of the request's method or path. It would be more useful if your HTTP server could send back different HTTP responses based on the information inside the HTTP requests.

Let's fix that by refactoring the server.js file with the following code.

const http = require('http');

const server = http.createServer(function(req, res) {
  if (req.method === 'GET' && req.url === '/guests') {
    let guests = ['Mary', 'Don'];
    let guestsJSON = JSON.stringify(guests);

    res.setHeader('Content-Type', 'application/json');
    res.end(guestsJSON);
  } else {
    res.statusCode = 404;
    res.setHeader('Content-Type', 'text/plain');
    res.end('Not found');
  }
});

server.listen(8000, function() {
  console.log('Listening at localhost:8000');
});

Now, save the server.js file, terminate the existing server with Ctrl + C, and run it again with the node command.

node server.js

And you should see something like this.

Listening...

In a separate Terminal tab, send the following HTTP request to the server.

http GET localhost:8000/

And you should see something like this.

Not Found

In a separate Terminal tab, send the following HTTP request to the server.

http GET localhost:8000/guests

And you should see something like this.

Json res

Nodemon

Manually restarting a Node.js HTTP server gets old fast. Plus, it's easy to forget to do it every time you refactor your code. To speed up your development workflow, let's use a command-line utility, called Nodemon, that'll run your server with Node.js and automatically restart it when the code changes.

Initialize a package.json file and install nodemon.

npm init -y
npm install --save nodemon

NPM start script

Let's take advantage of a built in entry script for _start_ing up an application provided by NPM, npm start. By default it is set to node server.js, but we're going to change that to nodemon.

// package.json

"scripts": {
  "test": "echo \"Error: no test specified\" && exit 1",
  "start": "./node_modules/.bin/nodemon server.js"
},

Terminate the existing server with Ctrl + C, but this time run it with the npm start command.

npm start

And you should see something like this.

Listening...

Send the following HTTP request to the server to verify everything works the same.

http GET localhost:8000/guests

JSON data

Guests is currently a hardcoded array. Let's fix that by refactoring the server.js file to pull data from a JSON file.

const path = require('path');
const http = require('http');
const fs = require('mz/fs');
const guestsPath = path.join(__dirname, 'guests.json');

const server = http.createServer(function(req, res) {
  if (req.method === 'GET' && req.url === '/guests') {
    fs.readFile(guestsPath, 'utf8')
      .then(function(guestsJSON) {
        res.setHeader('Content-Type', 'application/json');
        res.end(guestsJSON);
      })
      .catch(function(err) {
        console.error(err.stack);
        res.statusCode = 500;
        res.setHeader('Content-Type', 'text/plain');
        return res.end('Internal Server Error');
      });
    } else {
      res.statusCode = 404;
      res.setHeader('Content-Type', 'text/plain');
      res.end('Not found');
    }
});

server.listen(8000, function() {
  console.log('Listening at localhost:8000');
});

Path

In the code above we're importing Node's path module. This module is used to build paths out based on the operating system running the application. On Windows path segments are delineated by a backslash and on Linux by a forward slash. The join() method takes path segments as arguments and concatenates the provided segments using the platform specific delimiter. Lastly we have __dirname which will preface the path with the absolute path from which the Node application is running.

fs (Promise-ified)

The other thing we're importing is the mz/fs package. This is a packge that wraps Node's fs module with Promises. The interface is the same with the exception that in place of having a callback function we can use .then() and .catch().

Since mz is a package from NPM, you will want to install it before you move on:

npm install --save mz

Now, save the server.js file and add the following data to the guests.json file.

echo '["Mary", "Don"]' > guests.json

Send the following HTTP request to the server.

http GET localhost:8000/guests

And you should see something like this.

Json res

Single resources

Right now, your HTTP server can only send back all the records from the database. It would be much more useful if your HTTP server could send back individual records as well.

Let's fix that by refactoring the server.js file with the following code.

const path = require('path');
const http = require('http');
const fs = require('mz/fs');
const guestsPath = path.join(__dirname, 'guests.json');

const server = http.createServer(function(req, res) {
  if (req.method === 'GET' && req.url === '/guests') {
    fs.readFile(guestsPath, 'utf8')
      .then(function(guestsJSON) {
        res.setHeader('Content-Type', 'application/json');
        res.end(guestsJSON);
      })
      .catch(function(err) {
        console.error(err.stack);
        res.statusCode = 500;
        res.setHeader('Content-Type', 'text/plain');
        res.end('Internal Server Error');
      });
  } else if (req.method === 'GET' && req.url === '/guests/0') {
    fs.readFile(guestsPath, 'utf8')
      .then(function(guestsJSON) {
        var guests = JSON.parse(guestsJSON);
        var guestJSON = JSON.stringify(guests[0]);

        res.setHeader('Content-Type', 'application/json');
        res.end(guestJSON);
      })
      .catch(function(err) {
        console.error(err.stack);
        res.statusCode = 500;
        res.setHeader('Content-Type', 'text/plain');
        res.end('Internal Server Error');
      });
  } else if (req.method === 'GET' && req.url === '/guests/1') {
    fs.readFile(guestsPath, 'utf8')
      .then(function(guestsJSON) {
        var guests = JSON.parse(guestsJSON);
        var guestJSON = JSON.stringify(guests[1]);

        res.setHeader('Content-Type', 'application/json');
        res.end(guestJSON);
      })
      .catch(function(err) {
        console.error(err.stack);
        res.statusCode = 500;
        res.setHeader('Content-Type', 'text/plain');
        res.end('Internal Server Error');
      });
  } else {
    res.statusCode = 404;
    res.setHeader('Content-Type', 'text/plain');
    res.end('Not found');
  }
});

server.listen(8000, function() {
  console.log('Listening at localhost:8000');
});

Now, save the server.js file and send the following HTTP request to the server.

http GET localhost:8000/guests

And you should see something like this.

Json res

Now, send the following HTTP request to the server.

http GET localhost:8000/guests/0

And you should see something like this.

Json res

Finally, send the following HTTP request to the server.

http GET localhost:8000/guests/1

And you should see something like this.

Json res

Bonus - Extending Routes

Using an if statement works for our two routes (/guests and /guests/:id), but it doesn't scale well if we have many routes. Instead, we can create a file named routes.js and use an object to define routes in a more scalable way.

// routes.js

routes = {
  '/special-message': function(req, res) {
    res.end("You're SPECIAL");
  },

  '/non-special-message': function(req, res) {
    res.end("You're boring!");
  }
};

module.exports = routes;
// index.js
var http   = require('http');
var routes = require('./routes');

var handleRequest = function (req, res) {
  if(routes[req.url] !== undefined) {
    routes[req.url](req, res);
  } else {
    res.end("404, no such route");
  }
};

var server = http.createServer(handleRequest);

server.listen(8000, function() {
  console.log("Listening...");
});

If we submit a request to either localhost:8000/special-message or localhost:8000/non-special-message, we still receive our intended response.

Reading

Video

Dynamic Web App Concepts How does the Internet work?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment