#100DaysOfMERN - Day 22

#100DaysOfMERN - Day 22


9 min read

Continuing from yesterday, where I set up two servers - one with Vanilla JS and one with Express:

✏ Serving Static Files with Vanilla Server

You usually wouldn't hardcode a <h1> into the response of your server, but instead serve a nicely styled HTML document. So I'll add a vanilla.html file to my folder (with a 🤍 as favicon):

<!DOCTYPE html>
<html lang="en">
    <meta charset="UTF-8">
    <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2280%22>&#129293;</text></svg>">
<body style="font-family:sans-serif">
    <h1>I am a webpage!</h1>
    <p>brought to you with Vanilla JS</p>

Naively going to localhost:3002/vanilla.html won't bring me anywhere, instead the server tells me to check my spelling. I'm obviously very used to having all that stuff being handled for me under the hood, but I'll have to do that manually now.

First of all, I'll have to import two more things:

  • the fs module = File System API, which I've already encountered on Day 18 - How Not To Make A Post Request, where I used it to read and write a file to serve some data (I was pretty confident that my code was horrible and that what I was doing is certainly not how it should be done, but now I'm not so sure anymore)

  • the path module, which is only used to get the file extension from a string, so this isn't absolutely necessary, I could do that myself with some regex. It's just convenient to use it

As both are Node core modules, they can be imported without installing them first:


import http from 'http';
import fs from 'fs';
import path from 'path';

In my previous code, I just set up a switch statement to serve a different <h1> for a few routes. I'll completely delete that code now and replace it with something that can serve actual files.

✏ The File Path

I have no index.html, I've called that vanilla.html to distinguish it from the Express server, and I'd like to serve that now if someone accesses the root directory. Building the file path by adding a . at the beginning:

import http from 'http';
import fs from 'fs';
import path from 'path';

const app = http.createServer((req, res) => {

    let filePath = '.' + req.url;
    if (filePath === './'){
        filePath += 'vanilla.html'


app.listen(3002, console.log('vanilla is serving'))

✏ The File Extension and MIME types

An operating system like Windows usually takes the file extension to determine which application the file should be handled with. A browser functions differently and instead makes that decision based on the MIME type (Multipurpose Internet Mail Extension). To tell the browser how to handle a file, the MIME type needs to be included in the Response Header. There are tons of different types, but the most common are text/html, text/css, text/javascript, image/jpg, audio/mpeg etc.

In order to find out which MIME type I should set in the header, I'll grab the extension and get the corresponding MIME type from a list (I've included just the most common ones, the actual list would depend on all the files that your server is supposed to serve):

    const extension = String(path.extname(filePath)).toLowerCase();

    const mimeTypes = {
        '.html': 'text/html',
        '.js': 'text/javascript',
        '.css': 'text/css',
        '.json': 'application/json',
        '.png': 'image/png',
        '.jpg': 'image/jpg',
        '.gif': 'image/gif',
        '.svg': 'image/svg+xml'

    const contentType = mimeTypes[extension] || 'application/octet-stream';

If no MIME type can be found, the last line sets the contentType to a default application/octet-stream, which means unknown binary file. In most cases, browsers won't execute the file, but instead offer a download of the file with a "save as" dialogue.

If the MIME type is set to application/octet-stream and I'm serving a file with a <!DOCTYPE html> declaration, Chrome displays the page correctly, but also offers a download and informs me in the console: Resource interpreted as Document but transferred with MIME type application/octet-stream. Firefox will go directly to the "save as" dialogue.

✏ Serving the file

Once the file path and the extension/MIME type are determined, I'll need the (asynchronous)readFile method of the fs module. It takes the file path and a callback as parameters, and the logic within basically is similar to my former switch statement.

If there's no error (an error would be if no file exists for the given path), I'll let my server respond with status code 200 and send the file content as a stream of utf-8 encoded bytes, otherwise, I'll respond with a default 500 server error, and display the error message.

If you're interested in how that stream looks, this is the <!DOCTYPE html> declaration:

<Buffer 3c 21 44 4f 43 54 59 50 45 20 68 74 6d 6c 3e 0d 0a>

And the code of the function:

    fs.readFile(filePath, (err, content) => {
         if (!err){
            res.writeHead(200, 'OK', {
                'Content-Length': Buffer.byteLength(content),
                'Content-Type': contentType
        } else {
            res.end('ERROR: '+err)

And voilà - browser displays the page:


✏ Recap

I've learned

  • how to serve a static HTML file with a Vanilla JS server

✏ Next:

  • serving static files with Express

✏ Thanks for reading!

I do my best to thoroughly research the things I learn, but if you find any errors or have additions, please leave a comment below, or @ me on Twitter. If you liked this post, I invite you to subsribe to my newsletter. Until next time 👋

✏ Previous Posts

  • Day 1: Introduction, Node.js, Node.js in the terminal
  • Day 2: npm, node_modules, package.json and package-lock.json, local vs global installation of packages
  • Day 3: Create a React app without create-react-app, Webpack, Babel
  • Day 4: npx and cowsay
  • Day 5: npm vs. npx, npm audit, semantic versioning and update rules
  • Day 6: Call stack, event loop, JavaScript engine, JavaScript runtime
  • Day 7: Call stack and event loop in Node.js, setImmediate()
  • Day 8: setImmediate(), process.nextTick(), event loop phases in Node.js
  • Day 9: Network requests with XMLHttpRequest and callbacks
  • Day 10: Promises
  • Day 11: Network requests with XMLHttpRequest and Promises
  • Day 12: React Quiz App part 1
  • Day 13: React Hangman
  • Day 14: FullStackOpen course 1: details of GET and POST requests, request headers
  • Day 15: React Hangman: Trigger fetch with click event callback vs useEffect
  • Day 16: REST API and CRUD
  • Day 17: Boring Book App part 1: React Frontend, Express Backend, GET requests, CORS
  • Day 18: Boring Book App part 2: POST request, File System API
  • Day 19: Boring Book App part 3: Request Parameters, DELETE request
  • Day 20: Boring Book App part 4: PUT request
  • Day 21: Express JS vs Vanilla JS part 1: Server setup, routes