DRY node.js server code (without Express).

DRY

This is the code we’ll be starting with… a very basic Hello World node server.

//server.js
var http = require('http');
var server = http.createServer((request, response) => {
response.writeHead(200, {'Content-Type': 'text/plain'});
response.end('Hello, World!');
});
server.listen(8080);

We want to flesh out and refactor the server so that it can be easily extended i.e. handle multiple paths and request types.

The first thing we’ll want to do is to move the request handler callback code into its own file and export it. This makes it available for other files to import and use.

//server.js
var http = require('http');
// require the new module
var handler = require('./handler');
var server = http.createServer(handler);
server.listen(8080);
//handler.js
// create a file and export the function to be used as the request handler

module.exports = (request, response) => {
response.writeHead(200, {'Content-Type': 'text/plain'});
response.end('Hello, World!');
};

We now want to set up the handler for the different types of actions that we want to accept e.g. GET, POST.

//handler.js
module.exports = (request, response) => {
if (request.method === 'GET') {
response.writeHead(200, {'Content-Type': 'text/plain'});
response.end('Hello, World!');
} else if (request.method === 'POST') {
response.writeHead(200, {'Content-Type': 'text/plain'});
response.end('Success');
}
};

When handling POST requests we have to collect the data that is being sent as part of the request. This is done asynchronously. Let’s add that functionality to the POST section of the if else block.

//handler.js
module.exports = (request, response) => {
if (request.method === 'GET') {
response.writeHead(200, {'Content-Type': 'text/plain'});
response.end('Hello, World!');
} else if (request.method === 'POST') {
var data = '';
request.on('data', (chunk) => {
data += chunk;
};
request.on('end', () => {
response.writeHead(200, {'Content-Type': 'text/plain'});
response.end('Success');
};
}
};

We’re going to want to refactor this code because 1. we’re repeating code and that isn’t DRY 2. we’re going to be handling collecting data and sending responses quite a lot so it makes sense to move the code into their own functions. We use a callback function to keep track of the data collected during the post request.

//handler.js
var sendResponse = (response, data, statusCode, headers) => {
response.writeHead(statusCode, headers);
response.end(data);
};
var collectData = (request, callback) => {
var data = '';
request.on('data', (chunk) => {
data += chunk;
};
request.on('end', () => {
callback(data);
});
};
module.exports = (request, response) => {
if (request.method === 'GET') {
sendResponse(response, 'Hello World', 200, {'Content-Type': 'text/plain'});
} else if (request.method === 'POST') {
collectData(request, (formattedData) => {
// do something with the formatted data e.g. store in db
sendResponse(response, 'Success', 200, {'Content-Type': 'text/plain'});
});
}
};

If we wanted the server to handle additional routes or methods the if else part of the handler is going to get messy. Let’s refactor it by creating an object with the HTTP Verbs as the object’s keys. We can then invoke the function associated with the action.

//handler.js
var sendResponse = (response, data, statusCode, headers) => {...};
var collectData = (request, callback) => {...};
var actions = {
'GET': (request, response) => {
sendResponse(response, 'Hello World', 200, {'Content-Type': 'text/plain'});
},
'POST': (request, response) => {
collectData(request, (formattedData) => {
// do something with the formatted data e.g. store in db
sendResponse(response, 'Success', 200, {'Content-Type': 'text/plain'});
});
}
};
module.exports = (request, response) => {
var action = actions[request.method];
if (action) {
action(request, response);
} else {
// add catch all error handler
sendResponse(response, "Not Found", 404);
}
};

We now need to consider different endpoints / paths as well as query strings. To handle these scenarios we’re going to want to intercept the requests before they get to the request handler. We can do this by adding an anonymous function to the server that will dissect and processes the requests before invoking the handler function. Node provides us with a handy built in module called url that will help use parse the request url into useable parts (like the path name).

//server.js
var http = require('http');
var url = require('url);
var handler = require('./handler');
var server = http.createServer(function(request, response) {
var parts = url.parse(request.url);
if (parts.pathname === '/') {
handler(request, response);
} else {
// error handle unknown path
}
});
server.listen(8080);

In order for us to handle non-existent paths we want to be able to access the sendResponse function we wrote in the handler. As we’ll probably be needing the sendResponse and collectData functions throughout the server let’s remove and place them into their own file. We’ll need to export and require the file wherever we used these utility functions.

//server.js
var http = require('http');
var url = require('url);
var utils = ('./utilities');
var handler = require('./handler');
var server = http.createServer(function(request, response) {
var parts = url.parse(request.url);
if (parts.pathname === '/') {
handler(request, response);
} else {
utils.sendResponse(response, "Not found", 404);
}
});
server.listen(8080);
//handler.js
var utils = ('./utilities');
var actions = {
'GET': (request, response) => {
utils.sendResponse(response, 'Hello World', 200, {'Content-Type': 'text/plain'});
},
'POST': (request, response) => {
utils.collectData(request, (formattedData) => {
// do something with the formatted data e.g. store in db
utils.sendResponse(response, 'Success', 200, {'Content-Type': 'text/plain'});
});
}
};
module.exports = (request, response) => {
var action = actions[request.method];
if (action) {
action(request, response);
} else {
utils.sendResponse(response, "Not Found", 404);
}
};
//utilities.js
exports.sendResponse = (response, data, statusCode, headers) => {
response.writeHead(statusCode, headers);
response.end(data);
};
exports.collectData = (request, callback) => {
var data = '';
request.on('data', (chunk) => {
data += chunk;
};
request.on('end', () => {
callback(data);
});
};

There’s one last bit of refactoring that we can do. The server code is going to start getting messy when we start handling different endpoints and using more that 1 request handler. We can employ the same pattern that we used to refactor the actions to refactor the paths / routes.

//server.js
var http = require('http');
var url = require('url);
var utils = ('./utilities');
var handler = require('./handler');
var routes = {
'/': handler,
'/users': ...
};
var server = http.createServer(function(request, response) {
var parts = url.parse(request.url);
var route = routes[parts.pathname];

if (route) {
route(request, response);
} else {
utils.sendResponse(response, "Not found", 404);
}
});
server.listen(8080);

That’s it.