Setting up a webserver using nodejs is very easy. Using the HTTP-module simply create server, provide a handler function that will send some data back to client and start it on some port. But how difficult will it be to create one directly using the net module. The net module is in nodejs the way to build applications using the tcp protocol. HTTP is a protocol that is transferred within the tcp payload and it contains the application data.

Looking at the wikipedia page for HTTP we can see exactly what it contains. So using the first example from the net module documentation I can quickly build a webserver that return hello world.

1
2
3
4
5
6
7
8
const net = require('net');

const server = net.createServer(socket => {
socket.write('HTTP/1.1 200 OK\n\nhallo world')
socket.end((err)=>{console.log(err)})
});

server.listen(3000);

As you see, with only 6 lines of code it is possible to return some data back to the client. This code works and was tested using the curl command as well as with a web browser. Just using socket.write, we can fulfill the HTTP protocol and return some data, in this case hallo world.

Next we want to see, if we can accept some argument and make a greeting server. For that we need to parse the HTTP client request headers. Using the receiving sockets on data event, we can print the request:

1
2
3
4
5
6
7
8
9
10
11
const net = require('net');

const server = net.createServer(socket => {
socket.on('data',data=>{
console.log(data.toString());
socket.write('HTTP/1.1 200 OK\n\nhallo world');
socket.end((err)=>{console.log(err)});
});
});

server.listen(3000);

And here we have the recording from received from curl.

1
2
3
4
5
6
GET / HTTP/1.1
Host: localhost:3000
User-Agent: curl/7.58.0
Accept: */*


The fist word is the Method, followed by a path, then an HTTP version. The next lines are headers. Ending with two new lines. After that empty line, additional data could be send, we will handle that later. For now let us add some code to parse the information into a request object:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const net = require('net');

const server = net.createServer(socket => {
socket.on('data',data=>{
const [firstLine, ...otherLines] = data.toString().split('\n');
const [method, path, httpVersion] = firstLine.trim().split(' ');
const headers = Object.fromEntries(otherLines.filter(_=>_)
.map(line=>line.split(':').map(part=>part.trim()))
.map(([name, ...rest]) => [name, rest.join(' ')]));
const request = {
method,
path,
httpVersion,
headers,
}
console.log(request);
socket.write('HTTP/1.1 200 OK\n\nhallo world')
socket.end((err)=>{console.log(err)})
});
});

server.listen(3000);

This will show us the following request object on console, when we query the server using curl:

1
2
3
4
5
6
7
8
9
10
11
{
method: 'GET',
path: '/',
httpVersion: 'HTTP/1.1',
headers: {
Host: 'localhost 3000',
'User-Agent': 'curl/7.58.0',
Accept: '*/*',
'': ''
}
}

As you can see, the request object has the properties method, path, http version as well as the other headers. Now let’s accept a name in our path.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const net = require('net');

const server = net.createServer(socket => {
socket.on('data',data=>{
console.log(data.toString())
const [firstLine, ...otherLines] = data.toString().split('\n');
const [method, path, httpVersion] = firstLine.trim().split(' ');
const headers = Object.fromEntries(otherLines.filter(_=>_)
.map(line=>line.split(':').map(part=>part.trim()))
.map(([name, ...rest]) => [name, rest.join(' ')]));
const request = {
method, path, httpVersion, headers
}
console.log(request)
const name = request.path.split('/')[1];
socket.write(`HTTP/1.1 200 OK\n\nhallo ${name}`)
socket.end((err)=>{console.log(err)})
});
});

server.listen(3000);

Query this using curl looks like the following:

1
2
$ curl localhost:3000/Tobias
hallo Tobias

To build a rest API, it is common today, to receive data in the json body of the request. For that we need to split the headers from the body and parse the body content as json.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const net = require('net');

const server = net.createServer(socket => {
socket.on('data',data=>{
const [requestHeader, ...bodyContent] = data.toString().split('\r\n\r\n');

const [firstLine, ...otherLines] = requestHeader.split('\n');
const [method, path, httpVersion] = firstLine.trim().split(' ');
const headers = Object.fromEntries(otherLines.filter(_=>_)
.map(line=>line.split(':').map(part=>part.trim()))
.map(([name, ...rest]) => [name, rest.join(' ')]));

var body;
try {
body = JSON.parse(bodyContent);
} catch(err){/* ignore */}


const request = {
method,
path,
httpVersion,
headers,
body
};
console.log(request)
socket.write(`HTTP/1.1 200 OK\n\nhallo ${request.body.name}`)
socket.end((err)=>{console.log(err)})
});
});

server.listen(3000);

So, with just 30 lines of code we have build a webserver out of the net/tcp module in nodejs. From the request object we can easily read information we need to implement any further logic. With some tweaking it might even be possible to call the router from express.

But I am not going to do this. What you see here is the result of some weekend experiment. And a presentation how simple it is to work with the raw http protocol. For building web applications I will still use frameworks that are probably based on the http and https modules. Maybe you can be motivated to implement connectors to other protocols as well using the net and udp modules. These modules are definitely a strong tool, and I would prefer using a module based on those than modules using native self compiled modules.

Contents