Node.js Backend Development - Serving Static Files
In the previous article we finished implementing our own routing system. We also solved the problem of serving a 404 page when there is no pattern match with the requested URL. In this article we will learn about serving static files. We will put all our static files inside a specific directory and when they are requested they will be served automatically. As we have a routing system in place, we can create a pattern for it and an associated view or handler.
The code for all the articles can be found on the Github repository Learn Node.js With Sabuj. Code for each article is separated into branches on the repository and code for the current article can be found on the branch named "008_static_file_serving".
Though we now have a routing system, it serves requests by matching path with pattern, with an exact match system. If we keep it this way then we will have to write thousands of patterns for thousands of different combinations of URLs. That's not a practical solution. We want to change it so that it matches the initial part instead of complete equality matching. What do we do when we want to delegate the request on a view according the the initial pattern? We need to change the comparison. Let's get back to code:
responder.js
var url = require('url');
var routes = require("../routes").routes;
var view_404 = require('../routes').view_404;
exports.serveRequests = function(request, response){
const url_comps = url.parse(request.url, true);
console.log(request.method + " " + request.url);
var path = url_comps.pathname;
if (path.charAt(0) === '/'){
path = path.slice(1);
}
var found = false;
for (var i = 0; i < routes.length; i++)
{
var router = routes[i];
var pattern = router.pattern;
var handler = router.handler;
if (pattern.charAt(0) === '/'){
pattern = pattern.slice(1);
}
if (path.startsWith(pattern)){
handler(request, response, url_comps);
found = true;
break;
}
}
if (!found){
view_404(request, response, url_comps);
}
};
See the difference:
if (path.startsWith(pattern)){
...
}
It's just one line of code that we needed to change.
Now we are ready to implement our static file serving system. We will keep our static files in the directory called static in the project root. Let's create it and create a .gitkeep file inside of it, so that git does not discard the directory when there is no file inside of it.
Now create a file named static_files.js inside the views directory. We will export a function named static_files from inside of it.
static_files.js
exports.static_files = function(request, response, url){
// processing codes go here.
}
In the routes.js we need to import the function.
routes.js
var home = require("./views/home").home;
var form = require("./views/form").form;
var process_form = require("./views/process_form").process_form;
var view_404 = require("./views/view_404").view_404;
var static_files = require("./views/static_files").static_files;
var routes = [
{
pattern: '',
handler: home
},
{
pattern: 'form',
handler: form
},
{
pattern: 'submit-form',
handler: process_form
},
{
pattern: 'static/',
handler: static_files
}
]
exports.routes = routes;
exports.view_404 = view_404;
Notice that we put a forward slash at the end of the pattern. You should put it there too. Let's imagine that someone goes to the url: http://your-site.com/static_image_pages and this URL will be handled by static_files handler instead of the handler designed for it. So, to avoid such circumstances we want to put a forward slash so that we can guarantee that only static files form the static directory will be served.
Now, to send the contents of the file from the static directory we need to read it and write to the response. But imagine a situation where your file size is 1 GB and your server has only 512MB of RAM. What will happen? Your application or the server itself will crash. We’re used to reading the full content and serving when we were using HTML, but in this case we do not want to do so. We want to read and serve in chunks. So, we will use the function createReadStream() to stream file data in chunks.
var stream = fs.createReadStream(file_name);
As it is an asynchronous operation—we need to listen to the data event to use the data. We will also need to listen to the end event so that we can end our request.
var fs = require("fs");
exports.static_files = function(request, response, url){
var stream = fs.createReadStream(file_name);
stream.on('data', function(data){
response.write(data);
}).on('end', function(){
response.end();
})
}
Now, we need to determine the file name of the requested file. For now we are assuming that users will always use the pattern "static/" for static files. In some future article we can make this more dynamic too, but for most usual situations it's alright to use "static/" as the pattern.
var fs = require("fs");
exports.static_files = function(request, response, url){
var pathname = url.pathname;
if (pathname.charAt(0) === '/'){
pathname = pathname.slice(1);
}
var file_name = pathname.slice("static/".length);
file_name = "static/" + file_name;
var stream = fs.createReadStream(file_name);
stream.on('data', function(data){
response.write(data);
}).on('end', function(){
response.end();
})
}
Now put an image file named image.jpg in the static directory. Fire up the terminal, go to the url: http://localhost:5000/static/image.jpg and see what happens.
Oops! It just shows the homepage no matter what URL you put there. The bug is there because we used the startsWith() method and if an empty string is passed as the parameter it returns true for all strings. So, we need to put a guard before this block. We need to check for equality. So, our responder code becomes:
var url = require('url');
var routes = require("../routes").routes;
var view_404 = require('../routes').view_404;
exports.serveRequests = function(request, response){
const url_comps = url.parse(request.url, true);
console.log(request.method + " " + request.url);
var path = url_comps.pathname;
if (path.charAt(0) === '/'){
path = path.slice(1);
}
var found = false;
for (i = 0; i < routes.length; i++)
{
var router = routes[i];
console.log("Router: ")
console.log(router)
var pattern = router.pattern;
var handler = router.handler;
if (pattern.charAt(0) === '/'){
pattern = pattern.slice(1);
}
if (path === pattern){
handler(request, response, url_comps);
found = true;
break;
}else if(pattern !== '' && path.startsWith(pattern)){
handler(request, response, url_comps);
found = true;
break;
}
}
if (!found){
view_404(request, response, url_comps);
}
};
Look at these lines of codes:
if (path === pattern){
handler(request, response, url_comps);
found = true;
break;
}else if(pattern !== '' && path.startsWith(pattern)){
handler(request, response, url_comps);
found = true;
break;
}
Now our code is working perfectly. We can see the image on the browser. But there is also something incomplete with our code. Our code does not specify the content type of the response. We can work on this later.
Keep practicing with Node.js and keep your eyes on this blog for new updates.
About the Author
My name is Md. Sabuj Sarker. I am a Software Engineer, Trainer and Writer. I have over 10 years of experience in software development, web design and development, training, writing and some other cool stuff including few years of experience in mobile application development. I am also an open source contributor. Visit my github repository with username SabujXi.
Recent Stories
Top DiscoverSDK Experts
Compare Products
Select up to three two products to compare by clicking on the compare icon () of each product.
{{compareToolModel.Error}}
{{CommentsModel.TotalCount}} Comments
Your Comment