Abdessamad Ely Logo

Create a Simple Node.js Web Server for Static Files

Abdessamad Ely
Abdessamad Ely
Software Engineer
Reading time: 5 min read   •   Published on in Node.js   •   Updated on

A static web server is often needed for different purposes from serving static files to a static website using a standalone Node.js server.

The following is a step-by-step guide into how you can create a static web server using Node.js to serve common static files.

Setting up our Node.js project

Open your terminal and navigate to your workspace, when you want to create the project then create a new directory with mkdir static-web-server.

Now, lets initialize our project by going inside the newly created folder using cd static-web-server.

Then using npm init -y we will initialize our Node.js project, this creates the package.json file.

In our case, we don't need any external dependencies, but it's good to always initialize NPM for our Node.js project.

Creating the Static Web Server

Let's start by opening the project in our preferred code editor, mine is vscode, so I will open the project with code ./static-web-server.

Setting up an Node.js HTTP server

Go ahead and create a new file src/index.js with the following:

src/index.js
const http = require('node:http')

const PORT = 8000

http
  .createServer(async function (request, response) {
    response.writeHead(404, { 'Content-Type': 'text/html' })
    response.end('<em>notfound</em>\n', 'utf-8')
  })
  .listen(PORT)

console.info(`Server running at http://localhost:${PORT}`)

We're starting slow, we first created an http server, which will respond to any request with a 404 response.

Now, we have a file that we can use node.js to run, so let's add a script to the package.json file:

package.json:snippet
{
  "...": "...",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node src/index.js"
  },
  "...": "..."
}

Creating our Static Folder

The purpose of our server is to serve files from a specific directory, but we want:

  • To server files with the right content-type header
  • To respond with 404 page when the requested file doesn’t exist

In this tutorial, we will use as an example of serving an uploads folder, I will create a new directory with gitignore file:

uploads/.gitignore
*

I have added the following files to the uploads folder so we can test our server once done:

  • icon.svg
  • index.html
  • pexels-m-shah.jpg

The project structure should now look something like:

Project directory tree displaying folders and files

Serving files from a Directory

Now that we have everything we need, lets start by handling the following scenario:

  • User requested inexistent file
  • User requested a directory
  • User requested a valid static file

We'll include the code snippet required for each scenario, then combine all of them in w fully working static server.

User requested inexistent file

src/index.js:snippet
const http = require('node:http')
const path = require('node:path')
const { lstat } = require('node:fs/promises')

const PORT = 8000
const HOST = `http://localhost:${PORT}`

http
  .createServer(async function (request, response) {
    const pathname = new URL(request.url || '/', HOST).pathname
    const filePath = path.join(process.cwd(), 'uploads', pathname)

    try {
      await lstat(filePath)
      response.writeHead(200, { 'Content-Type': 'text/html' })
      response.end('<em>found</em>\n', 'utf-8')
      return
    } catch (err) {
      if (err.code === 'ENOENT') {
        response.writeHead(404, { 'Content-Type': 'text/html' })
        response.end('<em>notfound</em>\n', 'utf-8')
        return
      }
      console.error(e)
    }
  })
  .listen(PORT)

console.info(`Server running at ${HOST}`)

Here, we're reading the pathname from the request, which is in our case will be the filename for the requested file.

After that, we use lstat which tries to get information regarding the requested file, and throw an exception if the file doesn't exist.

Now, if we try to run npm start, the visit a valid file we'll get a 200 response with "found" text, otherwise we'll get 404 response with "notfound" text.

User requested a directory

The lstat can get information for both files and directories:

src/index.js:snippet
try {
  const stat = await lstat(filePath)
  if (stat.isDirectory()) {
    response.writeHead(401, { 'Content-Type': 'text/html' })
    response.end('<em>forbidden</em>\n', 'utf-8')
    return
  }

  /** previous code */
} catch (err) {
  /** previous code */
}

Continuing on the previous snippet, here using the lstat return value, we check if the filePath is not actually a file but a directory.

If that's the case we send back a 404 unauthorized response with "forbidden" text.

User requested a valid static file

Now that we covered edge case scenarios, let's focus on actually serving valid files.

There are two steps into serving files:

  • Mapping file extensions to MIME types
  • Reading file from disk and sending it in the response
src/index.js:snippet
const extname = 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',
  '.webp': 'image/webp',
  '.gif': 'image/gif',
  '.svg': 'image/svg+xml',
  '.wav': 'audio/wav',
  '.mp4': 'video/mp4',
  '.woff': 'application/font-woff',
  '.ttf': 'application/font-ttf',
  '.eot': 'application/vnd.ms-fontobject',
  '.otf': 'application/font-otf',
  '.wasm': 'application/wasm',
}
const contentType = mimeTypes[extname] || 'application/octet-stream'

Using the path.extname function we extract the file extension from the filePath, then we define a list of the supported MIME types.

Using both the file extension, and the mimeTypes object, we can calculate the right content-type header for the file.

We also fallback to application/octet-stream in case the mime type is not supported.

src/index.js:snippet
try {
  const content = await readFile(filePath)
  response.writeHead(200, { 'Content-Type': contentType })
  response.end(content, 'utf-8')
  return
} catch (err) {
  if (err.code == 'ENOENT') {
    response.writeHead(404, { 'Content-Type': 'text/html' })
    response.end('<em>notfound</em>\n', 'utf-8')
    return
  }

  console.error(e)
  response.writeHead(500)
  response.end('Server errorr\n')
}

Using the readFile function from node:fs/promises, we read the file content, and send it after setting the Content-Type header.

If something happens we handle the exception, which can be file doesn’t exist or inaccessible, otherwise we fallback to a 500 response and log the error.

Node.js Web Server for Static Files

The following is the final version of our static server:

src/index.js
const http = require('node:http')
const path = require('node:path')
const { lstat, readFile } = require('node:fs/promises')

const PORT = 8000
const HOST = `http://localhost:${PORT}`

http
  .createServer(async function (request, response) {
    const pathname = new URL(request.url || '/', HOST).pathname
    const filePath = path.join(process.cwd(), 'uploads', pathname)

    try {
      const stat = await lstat(filePath)
      if (stat.isDirectory()) {
        response.writeHead(401, { 'Content-Type': 'text/html' })
        response.end('<em>forbidden</em>\n', 'utf-8')
        return
      }
    } catch (err) {
      if (err.code === 'ENOENT') {
        response.writeHead(404, { 'Content-Type': 'text/html' })
        response.end('<em>notfound</em>\n', 'utf-8')
        return
      }
      console.error(e)
    }

    const extname = 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',
      '.webp': 'image/webp',
      '.gif': 'image/gif',
      '.svg': 'image/svg+xml',
      '.wav': 'audio/wav',
      '.mp4': 'video/mp4',
      '.woff': 'application/font-woff',
      '.ttf': 'application/font-ttf',
      '.eot': 'application/vnd.ms-fontobject',
      '.otf': 'application/font-otf',
      '.wasm': 'application/wasm',
    }
    const contentType = mimeTypes[extname] || 'application/octet-stream'

    try {
      const content = await readFile(filePath)
      response.writeHead(200, { 'Content-Type': contentType })
      response.end(content, 'utf-8')
      return
    } catch (err) {
      if (err.code == 'ENOENT') {
        response.writeHead(404, { 'Content-Type': 'text/html' })
        response.end('<em>notfound</em>\n', 'utf-8')
        return
      }

      console.error(e)
      response.writeHead(500)
      response.end('Server errorr\n')
    }
  })
  .listen(PORT)

console.info(`Server running at ${HOST}`)

This is a fully working Node.js web server for serving static files, built without any frameworks or dependencies.

Feel free to make yours, and adapt it to fit your project needs and requirements.