How To Create A Static File Server Without Any NPM Package
Created on March 7, 2022.
Table of Contents
Problem
- We have a collection of static assets (html,css,js,images,etc)
- We want to build a server to serve this files online
- How can do that in NodeJS without any npm package
Pre-requisite
- An intermediate level understanding of NodeJS and Javascript
- You have to be familiar with the idea of streams. click here to learn about NodeJS streams
- Understand HTTP protocol. click here to learn more about the HTTP protocol
What are Static Files?
Well i’m glad you asked, static files are files that can be stored and sent to a client on request. Examples of such files are html, css, js, png, ico, mp3, pdf, mp4 etc.
A static file server designed to deliver static files. Usually it involves preparing a special folder where any request to files within the folder and its sub folders are delivered to the client.
Example use of a static file server is for serving static html websites. As long as it has an index.html file. the remaining assets are requested relative to the index.html file by the browser. click here to see an example of how i served a site with the code in this tutorial
Folder structure
Our project should contain:
- index.js : this should house our application logic
- public/: this folder will hold our static assets we wish to serve
public/:
index.js
💡 The code is a gradual build up to the final working code, you can keep track of the curent section by paying attention to the comments within the code
Create an HTTP server to listen for incoming request
Ok, we’d start by creating an http server. NodeJS has a build-in http
module with methods for handling all things HTTP related, like in our case we would need the createServer
method. The http.createServer
method returns a http.Server
object , we would store that in a variable server
.
const http = require('http')
//create server
const server = http.createServer()
Start server
The server
contains a listen
method that we would call to start listening for incoming requests. We would pass the port we want our server to listen on to the listen
method
In our case we would choose port 5000
const http = require('http')
const server = http.createServer()
//start server
server.listen(5000)
Handle listening event.
Its often handy to log when your server has started listening, otherwise we might not be sure everything works as planned.
You might also want to do something else when your server starts listening.
Thankfully the server
object is an event emitter, and we can listen for the listening event.
const http = require('http')
const server = http.createServer()
server.listen(5000)
//handle server listening
server.on('listening',()=>console.log('listening on 5000'))
Handle incoming requests
The http.createServer
method accepts a callback function as parameter. The callback function we provide would be called whenever there is a new request.
The callback function would be called with two arguments.
- IncomingRequest object : this object contains information about the incoming request
- ServerReponse Object : this object contains properties and methods to structure a response.
In this case our callback would accept the IncomingRequest as req
and the ServerReponse as res
const http = require('http')
const server = http.createServer((req,res)=>{
//handle incoming requests
})
server.listen(5000)
server.on('listening',()=>console.log('static serving on 5000...'))
Setup Error Handling
We are going to be sending alot of errors during our code execution. It would be nice to have a function to help structure our errors. we create a sendError
function that takes errorCode
, errorMessage
and our serverResponse object res
.
On the function call, we would;
- set the
res.statusCode
to theerrorCode
- set response
Content-Type
Header totext/plain
- finally, send the response by calling the
res.end
method with ourerrorMessage
const http = require('http')
// handle error
function sendError(errorCode,errorMessage,res){
res.statusCode = errorCode
res.setHeader('Content-Type','text/plain')
res.end(errorMessage)
}
const server = http.createServer((req,res)=>{
})
server.listen(5000)
server.on('listening',()=>console.log('static serving on 5000...'))
Validate Method
For security reasons, its good practice to validate the HTTP method from the incoming request. Remember, we never trust the client.
The req
object has a method
property that contains the HTTP method from the client. Since we are building a Static File Server, we would reject any method that is not GET
to be on the safe side.
const http = require('http')
function sendError(errorCode,errorMessage,res){
res.statusCode = errorCode
res.setHeader('Content-Type','text/plain')
res.end(errorMessage)
}
const server = http.createServer((req,res)=>{
//validate method
if(req.method!="GET") return sendError(501,'Method Not Implemented',res)
})
server.listen(5000)
server.on('listening',()=>console.log('static serving on 5000...'))
Parse Url
Next, we want to parse the incoming URL correctly.
-
If the client requests
"/"
route to we want to change it"/index.html"
because we assume thats what they are talking about. -
if any extra query parameters are added. we want to trim it off, as that will cause issues for our relative paths. Example is if the client requests
"/index.html?name=prince"
. We don’t have any file in our public directory that has that name.
const http = require('http')
const server = http.createServer((req,res)=>{
if(req.method!="GET") return sendError(501,'Method Not Implemented',res)
//parse url
if(req.url=="/") req.url = "/index.html"
const url = req.url.toString().split('?')[0]
})
server.listen(5000)
server.on('listening',()=>console.log('static serving on 5000...'))
Construct & Verify file Path
we would construct our final file path by:
- getting the path of our public folder as
staticDir
- create our
filePath
by joining ourstaticDir
and the parsedurl
- for security reasons, double-check if our resulting
filepath
contains ourstaticDir
to avoid any foul play from clients requesting files outside our public folder
💡 We use the
path.join
method from NodeJS built-inpath
module for our joins instead of string concatenations to avoid path errors like // double slashes or forward \ and backward / slash crisis.
const http = require('http')
const path = require('path')
//static directory
const staticDir = path.join(__dirname,'public')
const server = http.createServer((req,res)=>{
if(req.method!="GET") return sendError(501,'Method Not Implemented',res)
if(req.url=="/") req.url = "/index.html"
const url = req.url.toString().split('?')[0]
//construct & verify file path
const filePath = path.join(staticDir,url)
if (filePath.indexOf(staticDir + path.sep) !== 0) return sendError(403,'Forbidden',res)
})
server.listen(5000)
server.on('listening',()=>console.log('static serving on 5000...'))
Set Content Type
Our server will be designed to handle any static file placed in the public folder, so we have to device a means of generating mime types from the file extension.
- we create an object
mimeTypes
to hold common mimetypes. - we extract the file extension by performing some string manipulation
- we set the content type by matching the corresponding
mimeTypes
, and if we have no match we would default totext/plain
const http = require('http')
const path = require('path')
// setup mime types
const mimeTypes = {
//plain text
txt: 'text/plain',
//webpage
html: 'text/html',
css: 'text/css',
//script
js: 'application/javascript',
//images
jpg: 'image/jpeg',
png: 'image/png',
gif: 'image/gif',
svg: 'image/svg+xml',
ico: 'image/x-icon'
};
const server = http.createServer((req,res)=>{
if(req.method!="GET") return sendError(501,'Method Not Implemented',res)
if(req.url=="/") req.url = "/index.html"
const url = req.url.toString().split('?')[0]
const filePath = path.join(staticDir,url)
if (filePath.indexOf(staticDir + path.sep) !== 0) return sendError(403,'Forbidden',res)
// set content type
const fileExt = filePath.split('.').slice(-1)[0]
res.setHeader('Content-Type',mimeTypes[fileExt]||"text/plain")
})
server.listen(5000)
server.on('listening',()=>console.log('static serving on 5000...'))
Stream file
To send the file to the client:
- we create a readable stream from our
filePath
- call the readable stream’s
pipe
method and pass our response to handle the streaming.
const http = require('http')
const path = require('path')
const fs = require('fs')
const mimeTypes = {
txt: 'text/plain',
html: 'text/html',
css: 'text/css',
js: 'application/javascript',
jpg: 'image/jpeg',
png: 'image/png',
gif: 'image/gif',
svg: 'image/svg+xml',
ico: 'image/x-icon'
};
const server = http.createServer((req,res)=>{
if(req.method!="GET") return sendError(501,'Method Not Implemented',res)
if(req.url=="/") req.url = "/index.html"
const url = req.url.toString().split('?')[0]
const filePath = path.join(staticDir,url)
if (filePath.indexOf(staticDir + path.sep) !== 0) return sendError(403,'Forbidden',res)
const fileExt = filePath.split('.').slice(-1)[0]
res.setHeader('Content-Type',mimeTypes[fileExt]||"text/plain")
// stream file
const fileStream = fs.createReadStream(filePath)
fileStream.pipe(res)
console.log('serving',req.url,'as',mimeTypes[fileExt])
})
server.listen(5000)
server.on('listening',()=>console.log('static serving on 5000...'))
Handle Streaming Errors
Of course errors might occur during streaming. so lets handle those by listening for error event on our fileStream
- The
404
(not found) error should occur if the requested file is not in our public directory. we would identify that error by inspecting the error object’scode
property forENOENT
(Error No Entity) code. - Any other error we would just send a
500
error something went wrong
⚠️ don’t send the error object to the client. this could give clues on the structure of your project. It is safer to leave them in the dark
const http = require('http')
const path = require('path')
const fs = require('fs')
const mimeTypes = {
txt: 'text/plain',
html: 'text/html',
css: 'text/css',
js: 'application/javascript',
jpg: 'image/jpeg',
png: 'image/png',
gif: 'image/gif',
svg: 'image/svg+xml',
ico: 'image/x-icon'
};
const server = http.createServer((req,res)=>{
if(req.method!="GET") return sendError(501,'Method Not Implemented',res)
if(req.url=="/") req.url = "/index.html"
const url = req.url.toString().split('?')[0]
const filePath = path.join(staticDir,url)
if (filePath.indexOf(staticDir + path.sep) !== 0) return sendError(403,'Forbidden',res)
const fileExt = filePath.split('.').slice(-1)[0]
res.setHeader('Content-Type',mimeTypes[fileExt]||"text/plain")
const fileStream = fs.createReadStream(filePath)
fileStream.pipe(res)
console.log('serving',req.url,'as',mimeTypes[fileExt])
// handle error
fileStream.on('error',error=>{
console.error(error)
//handle 404
if(error.code=="ENOENT") return sendError(404,'Not found',res)
//other errors
sendError(500,'something went wrong',res)
})
})
server.listen(5000)
server.on('listening',()=>console.log('static serving on 5000...'))
How to run the code
Run node index.js
command within the application directory to run the code.
Live demo
You can run the live demo on replit
Conclusion
So I hope this answers the question of how to build a Static File Server without any npm library.
I would still advice you use express or any other web server framework. as they will handles more issues that you won’t even think of and simplify your code alot. express literally has a single function express.static()
that does everything static files serving. But this knowledge helps you know how to extend these libraries if you want to.
See you later. Bye 👋🏾