It was the year 1989 when Sir Tim Berners-Lee proposed the vision for what we know as the Internet today. Even though the proposal wasn't very well received, he was granted time to work on the early version of the web and thus the coming months saw the first drafts of the HTML, URI, and HTTP and the first browser, WorldWideWeb.app as well as the first web server, httpd.

Since then, the early proposal has flourished to become a necessity for all human beings, and the httpd server went on to become the central idea around which web services are written today.

So, what is this server and why do we need it in the web architecture? Well, as Wikipedia states,

 A server is a computer program or a device that provides functionality for other programs or devices, called "clients".

A server is simply a remote computer that does computations for a user's requests and returns back the results. But how does the computer know what requests are incoming and how to listen to them all? Well, this is where server-side programming comes in.  Over the years we have had Java, Ruby on Rails, Django, PHP, ASP.NET, Apache HTTP Server and more competing to be the best in the business and it was great until the time when they weren't enough.

See all the frameworks I just mentioned are great at what they do, they have been industry favorites for a fair share of time but as the internet evolved, the need for a more dynamic server runtime emerged.

Think of a situation where we need to create a long-polling application. Our client application wants to update its content every few hundred microseconds. With the technologies, I mentioned a paragraph earlier, things can go south as each new client will create a new thread that will live up until the client application permanently stops requesting or asks to end the connection (keep-alive?). This may seem normal at the first glance but think of scaling such a system -- handling thousands of new clients per second -- the server can easily run out of resources or simply the C10K problem arises.

Multiple devices can poll the server for recent content concurrently.

Event-driven architectures like Node.js or Nginx are more than efficient at handling the C10K problem, in fact, they were built to solve the very issue. For the sake of this article, let's just focus only on what Node.js is, what it can do and how it does it all. We'll see more about Nginx in future posts.

So, what's Node.js after all?

Node.js was written in early 2009 by Ryan Dahl who had already become a critic of the limited possibilities of the existing web servers back then, primarily the inability to handle a lot of concurrent requests and the execution blocking style (sequential programming). He wrote Node.js with concurrency in mind, building it on top of a combination of Google Chrome's open source JavaScript engine V8 and an Event Loop. This very combination of V8 and event loop gives Node.js its core behavior -- ability to handle multiple concurrent requests on a single thread without blocking the I/O, but how?

Understanding the Event Loop

Everything in Node.js is an asynchronous task. From the very initialization of the input script to every callback in the system, everything is asynchronous in nature, that fire on either completion of a task or an event of failure.

The event loop is the main operator that offloads the asynchronous load from single-threaded JavaScript environment to the system kernel whenever possible. While most operating systems today provide the kernel interface for offloading asynchronous tasks, there can be a system that doesn't. For such a situation, Libuv, the library that provides event loop to Node.js, creates a pool of four threads for offloading async I/O tasks.

If you're interested in gathering more information on the internal working of Node.js, this might be helpful. We'll see more about the event loop and internal behavior of Node.js in a separate post.

Starting a Node.js server

There are actually two parts to writing a simple Node.js application. First, we need import the HTTP module provided by Node.js. Once the module is imported using either common js' require method or ECMAScript import, we need to initiate the server using createServer method which registers a callback function with arguments, request, and response.

const http = require('http')
const server = http.createServer((req, res) => {
    res.setHeader('Content-Type', 'text/plain')
    res.end('Hello, this is a sample server!')
})
server.listen(8080)

As I mentioned earlier, all the code that's executing in the Node.js environment is an async task. The very callback function the createServer method just registered with the event loop will be triggered everytime a new request hits the server and the request and the response objects are all prepared by either the thread pool or the async interface of the OS kernel.

What follows the callback is pretty straightforward. We take the request object (req), pull out the details required, if available and then push a response back to the originator of the request (the client) in reply to its demand(s) using res object. Simple isn't it? Maybe, or let's first see what these two callback objects are and then try to make sense out of them.

Understanding the Request object

Request, as the name suggests, is the handler for all the requests that hit the server. It's a plain JavaScript object with all the nitty-gritty details about the request in the target, more specifically the IP address of the client requesting, the hostname, headers, body (JSON or otherwise), params, query strings and more related information that make a request legitimate in all the server environments.

# reading the params in the request URL

const params = req.params

It's a server's responsibility to parse the request properly and appropriately respond to it. While Node.js does the parsing of the request, it's a programmer's job to read the request object and respond to it to complete the request-response transaction.

And the Response object

In order for a transaction to complete, a valid response must be sent back to the client. But what's valid and what's not? How to send a response back and what?

Node.js provides a minimal response API for eliminating such situations. Response object we receive as a callback for a request holds all the necessary methods for replying back to the client, be it a setHeader for setting response headers, or append for attaching data to response body or even the end method for finally ending the response process.

res.setHeader('Content-Type', 'text/plain')

While the use of predefined methods doesn't mean the response sent has to be valid, it certainly makes sure the error rate is kept to a minimum.

Why do we listen, at all?

The final piece of the puzzle is the listen method. What is it and why do we need it at all after initializing a server instance?

Well, if you took Networking classes, you might already know that while clients are arbitrary, a server is a single system whose address is known to every client that wants to connect to it. Now, in order to be present to all of them, a server implements a listen and accept architecture. Once a request is made, it's put in a queue and pulled out once the server is ready to accept it.

Here, listen method does the listening part in the Node.js architecture while the createServercallback can be treated as accept.

Listen takes in four parameters. First is the port number on which the Node.js process must run, with the second being the hostname that along with the port create a unique process identifier for the server.

const PORT = 8080,
      HOSTNAME = '127.0.0.1',
      BACKLOG = 10
      
server.listen(PORT, HOSTNAME, BACKLOG, () => {
    console.log('Server is now listening on', PORT)
})

The third and the fourth parameters of the method are default backlog for pending listener queue and callback for the successful beginning of listener, respectively.

While all the parameters listen accepts are optional, it is always better to assign values for port and hostname and if required, for backlog too. The reason for doing so are the default values. If the OS finds no custom port or it is set to 0, it assigns an arbitrary unused port to the process. The same goes with hostname. If the server finds no custom value for it, it starts accepting connections on the unspecified IPv4 address (0.0.0.0) or the unspecified IPv6 address (::), if available. Both of the default values are unacceptable in a production environment, whether running behind a proxy server or directly on HTTP (Port 80).

With this simple code setup we can easily start a very minimal server that returns a plain text -- "Hello, this is a sample server!" -- on every kind of request. Put together the pieces of code above in a js file (server.js) and run the node process like this,

node server.js

You will see that the script returns a log message -- "Server is now listening on 8080" -- as we defined in the listener callback. The best part of all is that Node.js will keep running and won't even eat up any CPU resources for listening, it will simply sleep unless of course there's a new request to be handled.

With that, we sum up this introduction of Node.js. We got to know what Node.js is, what caused an urgency for a single threaded server architecture like Node.js, what an event loop is, and how to start a simple HTTP server. We also got to know what the request and response objects are and how Node.js listens and accepts new requests and processes them to complete the request-response transaction with the help of an event loop. 

If you found any problem with the content of this article or a question, please be sure to comment below. I will be more than happy to respond to your queries or be corrected or both. Also, if you found this article helpful, please be sure to recommend it by tapping on the recommend button (star) below, or share it with your peers.