Node.js server without a framework
This article shows a static file server built in Node.js without using any frameworks. The current state of Node.js is such that almost everything we need for the static file server is provided by built-in APIs and a few lines of code.
Example
A static file server built with Node.js:
import * as fs from "node:fs";
import * as http from "node:http";
import * as path from "node:path";
const PORT = 8000;
const MIME_TYPES = {
default: "application/octet-stream",
html: "text/html; charset=UTF-8",
js: "application/javascript",
css: "text/css",
png: "image/png",
jpg: "image/jpeg",
gif: "image/gif",
ico: "image/x-icon",
svg: "image/svg+xml",
};
const STATIC_PATH = path.join(process.cwd(), "./static");
const toBool = [() => true, () => false];
const prepareFile = async (url) => {
const paths = [STATIC_PATH, url];
if (url.endsWith("/")) paths.push("index.html");
const filePath = path.join(...paths);
const pathTraversal = !filePath.startsWith(STATIC_PATH);
const exists = await fs.promises.access(filePath).then(...toBool);
const found = !pathTraversal && exists;
const streamPath = found ? filePath : STATIC_PATH + "/404.html";
const ext = path.extname(streamPath).substring(1).toLowerCase();
const stream = fs.createReadStream(streamPath);
return { found, ext, stream };
};
http
.createServer(async (req, res) => {
const file = await prepareFile(req.url);
const statusCode = file.found ? 200 : 404;
const mimeType = MIME_TYPES[file.ext] || MIME_TYPES.default;
res.writeHead(statusCode, { "Content-Type": mimeType });
file.stream.pipe(res);
console.log(`${req.method} ${req.url} ${statusCode}`);
})
.listen(PORT);
console.log(`Server running at http://127.0.0.1:${PORT}/`);
Breakdown
The following lines import internal Node.js modules.
import * as fs from "node:fs";
import * as http from "node:http";
import * as path from "node:path";
Next we have a function for creating the server. https.createServer
returns a Server
object, which we can start up by listening on PORT
.
http
.createServer((req, res) => {
/* handle http requests */
})
.listen(PORT);
console.log(`Server running at http://127.0.0.1:${PORT}/`);
The asynchronous function prepareFile
returns the structure: { found: boolean, ext: string, stream: ReadableStream }
.
If the file can be served (the server process has access and no path-traversal vulnerability is found), we will return the HTTP status of 200
as a statusCode
indicating success (otherwise we return HTTP 404
).
Note that other status codes can be found in http.STATUS_CODES
.
With 404
status we will return content of '/404.html'
file.
The extension of the file being requested will be parsed and lower-cased. After that we will search MIME_TYPES
collection for the right MIME types. If no matches are found, we use the application/octet-stream
as the default type.
Finally, if there are no errors, we send the requested file. The file.stream
will contain a Readable
stream that will be piped into res
(an instance of the Writable
stream).
res.writeHead(statusCode, { "Content-Type": mimeType });
file.stream.pipe(res);