My goal here is to introduce others to Node.js by basically detailing how I have learned it. In this initial post, I’ll jump right into creating a simple app-as-a-server and use that as a springboard for learning other aspects of Node.js.
- Learning Node.js, Part 1
- Learning Node.js, Part 2
- Learning Node.js, Part 3
- Learning Node.js, Part 4
- Learning Node.js, Part 5
To get started, make sure you install Node.js. I have instructions in the Setting up WebDev page.
Some Node Basics
Node.js is a server-side technology that’s based on Google’s V8 JavaScript engine. Node is designed to be used for applications that are heavy on input/output (I/O), but relatively light on intense (CPU-bound) computation. Node applications are created with JavaScript. The JavaScript you use is exactly the same JavaScript that you would use in your client-side applications. You can use an alternative language that compiles to JavaScript, such as CoffeeScript.
One of the fundamental design concepts with Node is that an application is executed on a single thread (or process) and all events are handled asynchronously. This asynchronous control is provided by an event loop and the liberal use of callback functions. An event loop is functionality that checks (“polls”) a system for specific events and invokes event handlers at the proper time. The “proper time” is when a certain event has occurred that a certain event handler is designed to respond to.
Important point there: in Node, a callback function is the event handler.
If you think of most web servers, like Apache or IIS, each time a web resource is requested, one of two things will happen: either a separate thread will be created to handle the request or a new process will be invoked to handle the request. Node doesn’t do either of those things. It doesn’t create a new thread nor does it create a new process for every request. Instead what Node does is listen for specific events, and when the event happens, it responds accordingly.
Another fundamental design aspect of Node is that doesn’t block any other request while its waiting for the event functionality it’s dealing with to complete. All events are handled in a relatively uncomplicated event loop. Unlike with single-threaded applications, when you make a request to a Node application and that application must, in turn, make some request of resources — like a database query request or a file access request — Node starts the request, but doesn’t just sit there waiting until the request receives a response. Instead, Node attaches a callback function to the request. When whatever has been requested has provided a response, an event is emitted to that effect. This event triggers the associated callback function to do something with either the results of the requested action (such as display some text) or the resources requested (such as querying a database).
A Node App-as-a-Server
Node offers an interesting conceptual model to work with. The idea is that the application that you write is also the web server. What Node does is provide a framework for you to build a web server that seamlessly serves up your application.
Let’s test it out. Create a file called testNode.js and put the following in it:
1 2 3 4 5 6 7 8 9 |
var http = require('http'); http.createServer(function(req, res) { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.write('Node is working.'); res.end(); }).listen(3000); console.log('Server started on localhost:3000; Ctrl-C to terminate.'); |
Most Node functionality is provided through external applications and libraries called modules. Case in point, line 1 loads the http
module, assigning it to a local variable so that I can call methods on it. The http
module provides the standard HTTP protocol functionality, which basically enables the application to have network access and accept HTTP request/response traffic.
Let’s break down what’s happening with this code:
- Line 3 actually creates a new web server. It does this using the
createServer()
method. The anonymous function that is passed as an argument to this method is a “request listener” function. It has two parameters: a server request calledreq
(of type http.ServerRequest) and a server response calledres
(of type http.ServerResponse).
- Line 4 uses the
http.ServerResponse.writeHead()
method to send a response header with the response status code of 200 as well as the content-type of the response. You’ll notice here that even though I’m creating a web server, I’m simply serving up plain text.
- Line 5 uses the
http.ServerResponse.write()
method to send a chunk of data to the response body. In this case, that chunk of data is a string of text.
- Line 6 uses the
http.ServerResponse.end()
method to signal that communication has finished. “Finished” here means all headers and the response body has been sent.
Before trying this out, let’s briefly consider a bit of the asynchronous nature of Node. The http.Server.listen()
method chained at the end of the createServer() method listens for incoming connections on a given port, which I’ve specified to be 3000. The listen method is asynchronous, which means that the application doesn’t block program execution while it’s waiting for the connection to be established. Whatever code follows the listen() call in the file will be processed without delay. The listen() callback function will be invoked when the port connection is established.
Make sure you understand this: the listen() method tells the HTTP server object to begin listening for connections on the given port. But Node doesn’t block while it waits for the connection to be established.
To make this a little more clear, let’s do a slight modification to the above code:
1 2 3 4 5 6 7 8 9 |
var http = require('http'); http.createServer(function(req, res) { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.write('Node is working.'); res.end(); }).listen(3000, function() { console.log('Start Server'); }); console.log('Server started on localhost:3000; Ctrl-C to terminate.'); |
There I’ve added a second parameter to the listen() method. The idea here is that if I need to do something once the connection is established, I provide a callback function. Here I’m passing an anonymous callback function to the listen() method. When the connection is established, a listening event is emitted, which then invokes the callback function. In this case, that callback function does nothing more than output a message to the console.
Run your app-server with this command
$ node testNode.js
Of interest here, you’ll see output like this:
$ node testNode.js Server started on localhost:3000; Ctrl-C to terminate. Start Server
Note here the order that the console.log statements clearly fired in. The “Start Server” message comes after the “Server started on…” message. That shows you that Node began listening for the connection and then immediately executed what was in the rest of my JavaScript file, which was simply to log a message saying “Server started on localhost:3000; Ctrl-C to terminate.” Then, once the connection was established, the callback was fired and the message “Start Server” was logged.
Incidentally, I could have made my logic a bit simpler by combining the write() and the end() statements:
1 2 3 4 5 6 7 8 |
var http = require('http'); http.createServer(function(req, res) { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end('Node is working'); }).listen(3000); console.log('Server started on localhost:3000; Ctrl-C to terminate.'); |
Do note there how I’ve gone back to my original listen() method.
Monitoring for Changes
Incidentally, before you make this change, consider this: if you make a change to your testNode.js file, you have to stop the server and then restart to see that change. That can get annoying really quick. I recommend installing a code monitoring module that will check if the code has changed and, if it has, will restart the server automatically. I’m using nodemon which you can install like this:
$ npm install -g nodemon
You can use forever, node-dev or node-supervisor as well. They all work pretty much the same way.
npm is the package manager for Node packages. The npm tool is not, according to the npm FAQ, an acronym for Node Package Manager. Apparently it’s a “recursive bacronymic abbreviation for “npm is not an acroynm.” Whatever.
The -g flag tells npm to install the nodemon package globally, meaning it’s available globally on the system and thus available to any Node app I create. Incidentally, if you’re wondering when you would install globally versus when you would not, the general rule of thumb is that JavaScript utilities (like nodemon) will generally be installed globally, whereas packages that are specific to your web app or project will not.
As a note to Windows users, when you specify the -g option, the packages are installed in a subdirectory of your Windows home directory that essentially involves your username. There are numerous reports that a lot of these packages don’t perform well if there are spaces in your username.
To try this, start your application like this:
$ nodemon testNode.js
You should see output like this:
14 Aug 06:46:54 - [nodemon] v1.2.1 14 Aug 06:46:54 - [nodemon] to restart at any time, enterrs
14 Aug 06:46:54 - [nodemon] watching: *.* 14 Aug 06:46:54 - [nodemon] startingnode testNode.js
Server started on localhost:3000; Ctrl-C to terminate.
Now make the change I mentioned above in the last code example: remove the res.write() line and modify the res.end() line. Upon saving the changes in testNode.js file, you should see the following output:
14 Aug 06:46:54 - [nodemon] v1.2.1 14 Aug 06:46:54 - [nodemon] to restart at any time, enterrs
14 Aug 06:46:54 - [nodemon] watching: *.* 14 Aug 06:46:54 - [nodemon] startingnode testNode.js
Server started on localhost:3000; Ctrl-C to terminate. 14 Aug 06:47:14 - [nodemon] restarting due to changes... 14 Aug 06:47:14 - [nodemon] startingnode testNode.js
Server started on localhost:3000; Ctrl-C to terminate.
As you can see, the server automatically restarted due to the change you made to the file.
The Event-Driven Model
Make sure you have at least some understanding that one of the core driving principles behind Node is that of event-driven programming. What that means is that you have to understand what events are available to you and how you can have your server/app respond to them. In the previous code example, the event is an HTTP request. The createServer() method takes a function as an argument. That function is invoked every time an HTTP request (event) is made. This simple program does nothing more than set the content type to plain text and sends the string “Node is working.”
While simple, this is you writing your first web server, with Node.js at least. That’s pretty cool. This particular server doesn’t serve HTML; rather, it just transmits the message “Node is working” in plain text to your browser. If you want, you can experiment with sending HTML instead, you could modify your example like this:
1 2 3 4 5 6 7 8 |
var http = require('http'); http.createServer(function(req, res) { res.writeHead(200, { 'Content-Type': 'text/html' }); res.end('<h1>Node is Working</h1>'); }).listen(3000); console.log('Server started on localhost:3000; Ctrl-C to terminate.'); |
The key differences there are pretty clear, I think. You have to send a header indicating the content is HTML (“text/html”) and you can include HTML in your string that is sent to the response body.
The event driven model, however, remains the same.
Explore the Asynchronous
Now stop your running server with Ctrl+C. The reason for this is because I’m going to ask you to write a different file and we’re going to use the new application we write to call up the existing testNode.js file. I’m doing this because I want to demonstrate Node’s asynchronous nature by creating a file that opens up the previously created testNode.js and outputs the contents to the client.
To get started, create a file called readTestNode.js. In that file, start with this:
1 2 3 4 5 6 |
var http = require('http'); http.createServer(function(req, res) { }).listen(3000); console.log('Server started on localhost:3000; Ctrl-C to terminate.'); |
So far that’s the start of what you did before. I’m going to build this file a bit more incrementally. First let’s add a new module:
1 2 3 4 5 6 7 |
var http = require('http'); var fs = require('fs'); http.createServer(function(req, res) { }).listen(3000); console.log('Server started on localhost:3000; Ctrl-C to terminate.'); |
A new module, File System (fs), is required. The File System module wraps standard file handling functionality. This includes opening and closing files, accessing the contents of a file, and so on. Now add the following:
1 2 3 4 5 6 7 8 9 10 |
var http = require('http'); var fs = require('fs'); http.createServer(function(req, res) { fs.readFile('testNode.js', 'utf8', function(err, data) { }); }).listen(3000); console.log('Server started on localhost:3000; Ctrl-C to terminate.'); |
The method used from the fs module is readFile(). Here that method is passed the name of the file to open, the encoding of that file, and an anonymous function which, by now, you know is the callback function. That callback function attached to the readFile() method is an instance of asynchronous behavior. Why do that here? Well, consider that access a file can be a time-consuming operation. If you have lots of clients hitting your server trying to access the same file, and if your server blocks on file access, you can imagine the user experience is going to be less good the more people there are using your app.
With this example, that’s not a problem because the file is opened and the contents are read asynchronously. Only when the contents have been read into the data buffer — or an error occurs during the reading process — is the callback function that was passed to the readFile() method invoked. That function will be passed the error (if any) or the data from the readFile action if no error occurs. So now let’s write the contents of the function:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
var http = require('http'); var fs = require('fs'); http.createServer(function(req, res) { fs.readFile('testNode.js', 'utf8', function(err, data) { res.writeHead(200, { 'Content-Type': 'text/plain' }); if (err) res.write('Unable to find or open file for reading\n'); else res.write(data); res.end(); }); }).listen(3000); console.log('Server started on localhost:3000; Ctrl-C to terminate.'); |
In the callback function, the error is checked, and if there is no error, the data is then written out to the response back to the client. To see how it works, start it up, using either node or nodemon:
$ node readTestNode.js
Going to http://localhost:3000 should show you that the contents of testNode.js are put into the browser. To test the error handling, of course, you could just use the wrong file name in line 5 in readTestNode.js.
I should note that Node isn’t all asynchronous function calls. Some objects may provide both synchronous and asynchronous versions of the same function. For example, there is a readFileSync() method that would essentially block all further processing until the file in question has been read entirely. However, I’m not sure why you would want to use that.
This was a hopefully useful start to this series of posts. Next I’ll start looking at making the app/server a little more familiar by considering how routing works and how to serve up static resources.