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:
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:
{
"...": "...",
"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:
*
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:

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
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:
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
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.
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:
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.