Static Content Server with Nodejs without frameworks
Tags: node, javascript, beginners, tutorial
Published: 2021-09-21 11:14:14

# Static server with Nodejs
This tutorial will walk you through few steps how to set up simple http server for static content using only **nodejs**. We will add basic features as serving requested resource from file or from memory(cache) and responding with error message when no resource is available. 
In reality you will almost never run any http server in this manner however, it might be very helpful to understand what frameworks like **Expressjs** do under the hood. It also could serve as a very simple testing tool in your local environment.

Requirements is to have installed `nodejs` on the system, preferably newer version (12+). The recommended environment is Unix like machine, but it is not necessary. The target audience is javascript beginner or UI developer who is curious how does http server work in nodejs.

We will go through the following:

- setup http server, what is static server
- adding rules how to read the request
- finding resources and caching

## Lets start with the simplest possible
Http server is a network application which listens to incoming network traffic. It is doing so by acquiring some system resources. Specifically it creates the process in the memory which listens to incoming traffic over the network on the dedicated port. To talk to the http server we need the physical address of the computer and the port which the application acquired. Nodejs provides all necessary functionality to do so. Let's have a look how nodesj does it.

the simplest way how to start and run the most basic http server using nodejs would be something like this:
```sh
node -e "require('http').createServer((req, res) => {res.end('hello world')}).listen(3000)"
```
Running the above code on Linux machine with node installed will start the server.
it can be verified by typing `http://localhost:3000` in the browser URL bar.
or by typing the following in new terminal window:
```sh
> curl http://localhost:3000
// expected response is
hello world
```

In this basic example we can easily see the building stones. We create an object and call the listen which effectively opens up the connection on the given port and it is waiting for the incoming request complying HTTP protocol.
We can test it with `netcat` sending a text complying with HTTP GET request header.
```bash
printf "GET / HTTP/1.1\r\n\r\n" | nc 127.0.0.1 3000
// The expected response is again
HTTP/1.1 200 OK
Date: Tue, 21 Sep 2021 09:59:13 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Content-Length: 11

hello world%
```
It is a little richer because **netcat** prints just about everything what is received in the response including response header. `curl` can do it as well. Go ahead and try using `-i` flag.

The other main component aside `createServer()` and `listen()` is callback passed into [createServer](https://nodejs.org/dist/latest-v16.x/docs/api/http.html#http_http_createserver_options_requestlistener). It contains references to [request](https://nodejs.org/dist/latest-v16.x/docs/api/http.html#http_class_http_clientrequest) and [response](https://nodejs.org/dist/latest-v16.x/docs/api/http.html#http_class_http_serverresponse) objects. Working with these two objects we can interact with our http server.

This article is however not about networking and protocols but tutorial how to build simple static content server using only **nodejs** and this does not get us too far since it responses with "hello world" to any request. Let's see if we can do better.

## Serving response from a file
Let's make one step further in terms of the functionality of our new http server. We are aiming towards the server which can serve static content. The word static here means similar to "static" keyword in javascript. It is something which is already known and defined prior the user request. From the web server we usually referred as static content to files like images, icons, CSS files and so on. So let's server user with the content of the file rather then hard coded message.
```js
module.exports = function staticServer() {
  const path = './static_content';
  const port = 3000;

	// create server object as in previous example
	var server = http.createServer(function(req, res){
    const filePath = path + 'index.html';
    fs.readFile(absPath, function(err, data) {
      res.end(data);
    });
	});

  server.listen(port, function() {
    console.log("server listening on port: " + port));
  });
  return server;
};
```
in addition, create directory and file `./static_content/index.html` containing your content:
```
<html>
  <body>
    <h1>
      Hello, this is very simple
    </h1>
  </body>
</html>
```

In the above code we define the path where the static content is, in this case it is index.html file we read the file and send the data back to user as a response to client's request.
`response.end() executes the above with some [default headers]()`

## Finding and serving requested resource
Next in the quest serving the content based on the user request is finding the requested resource our user is asking. The server looks it up and if it exists it serves the content of the file to client.

```js
module.exports = function staticServer() {
  const path = './static_content';
  const port = 3000;

	// create server object as in previous example
	var server = http.createServer(function(req, res){
    // get the resource from request
    const filePath = path + req.url;
    fs.readFile(absPath, function(err, data) {
      res.end(fileContents);
    });
	});

  server.listen(port, function() {
    console.log("server listening on port: " + port));
  });
  return server;
};
```
`const filePath = path + req.url` show how mapping between the requested resource and the actual resource might work. `Path` is relative path to location where our nodejs app is running and `req.url` is last bit of the URI identifying what resource user wants.

http://www.example.com/**resource**

### Caching
Let's make one small addition. The cache. When we server the file from a disk it is not a big deal, as it is pretty quick, however if the file would come from some more time expensive resource we want to keep the content of the file for later requests. Here is a very simple example how it can be implemented:
```js
module.exports = function staticServer() {
  const path = './static_content';
  const port = 3000;

  const cache = {}

	// create server object as in previous example
	var server = http.createServer(function(req, res){
    // get the resource from request
    const filePath = path + req.url;
    if (cache[filePath]) {
      sendFile(res, filePath, cache[filePath]);
    } else {
      fs.readFile(filePath, function(err, data) {
        res.end(fileContents);
      });
    }
	});

  server.listen(port, function() {
    console.log("server listening on port: " + port));
  });
  return server;
};
```

## Basic error handling and wrap up
In this last section we add some simple error handling. In case the user specifies the resource which is not found in the given location of static content or if the resource in not readable we need to notify the user with an error. The standard way of doing it is to return response with code 404 in the response headers. We also might add some explanation in the content.
```js
let 
	fs = require('fs'),
	path = require('path'),
	http = require('http');

const cache = {};

/**
 * lookup content type
 * infer from the extension
 * no extension would resolve in "text/plain"
 */
function lookupContentType(fileName) {
  const ext = fileName.toLowerCase().split('.').slice(1).pop();
  switch (ext) {
    case 'txt':
      return 'text/plain';
    case 'js':
      return 'text/javascript'
    case 'css':
      return 'text/css'
    case 'pdf':
      return 'application/pdf';
    case 'jpg':
    case 'jpeg':
      return 'image/jpeg';
    case 'mp4':
      return 'video/mp4';
    default:
      return ''
  }
}


/**
 * plain 404 response
 */
function send404(res){
	res.writeHead(404, {'Content-Type':'text/plain'});
	res.write('Error 404: resource not found.');
	res.end();
}

/**
 * sending file response
 */
function sendFile(res, filePath, fileContents){
	res.writeHead(200, {"Content-Type": lookupContentType(path.basename(filePath))});
	res.end(fileContents);
}

/**
 * serve static content
 * using cache if possible
 */
function serveStatic(res, cache, absPath) {
  // use cache if there is any
	if (cache[absPath]) {
		sendFile(res, absPath, cache[absPath]);
	} else {
		fs.exists(absPath, function(fileExists) {
      // attempt to read the resource only if it exist
			if (fileExists) {
				fs.readFile(absPath, function(err, data){
          // not able to read the resource
					if(err) {
						send404(res);
					} else {
						cache[absPath] = data;
						sendFile(res, absPath, data);
					}
				});
			} else {
        // resource does not exist
				send404(res);
			}
		});
	}
}

module.exports = function startServer(spec){
  let { path, port } = spec;

	// create server object
	var server = http.createServer(function(req, res){
    // if no resource is specified use index.html
		if(req.url === '/') {
			const filePath = path + 'index.html';
      serveStatic(res, cache, filePath);
		} else {
      const filePath = path + req.url;
      serveStatic(res, cache, filePath);
		}
	});

  server.listen(port, function(){
    console.log("server listening on port: "+port);
  });
  return server;
};

```
Now we can run it like this:
```js
const startServer = require('./startServer.js')

startServer({ path: './static_content', port: 3000 });
```
In the above example I added very basic error handling. In the event the resource specified by the user is not found in the static content directory, or it can't be open for reading, the server response with different header with error [code 404](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status) and different content explaining what went wrong.
In order for a browser to understand better what kind of content we are dealing with, it is also a good idea to include some indication about resource [content type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types). In `lookupContentType` we can do it just based on the file extension type.
Now if we try `pdf` the browser will have no problem opening pdf file instead downloading it.

## Conclusion
This is by no means a robust product, merely a very simplified example how things do work behind the curtain in frameworks like `expressjs`. We leveraged the nodejs built in library `http` to run simple http server.
We implemented simple routing to find static content in a given location. We also implemented simple in memory caching, content type resolution and basic error handling in case the resource is not found or accessible.

## Further reading
 If anybody want to build their own server serving static content I would recommend using existing framework. I would also strongly advice looking at least into following topics:
 - session and transaction management
 - caching
 - security, authentication and authorisation

## Sources
1. [nodejs/http](https://nodejs.org/dist/latest-v16.x/docs/api/http.html)
2. [netcat](https://www.unix.com/man-page/Linux/1/netcat/)
3. [http](https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol)
4. [status codes](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status)
5. [Common MIME types](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types)
6. [title image](https://pixabay.com)