Skip to content

Instantly share code, notes, and snippets.

@higuma
Last active April 19, 2026 06:38
Show Gist options
  • Select an option

  • Save higuma/3a851c9f7d4a87583f00642e0292153d to your computer and use it in GitHub Desktop.

Select an option

Save higuma/3a851c9f7d4a87583f00642e0292153d to your computer and use it in GitHub Desktop.
Lightweight node.js server
#!/usr/bin/env node
// Lightweight node.js server
// Author: Yuji Miyane (https://github.com/higuma)
// Date: 2026-04-19
import fs from 'node:fs';
import http from 'node:http';
import path from 'node:path';
// Supported MIME types
const MIME_TYPES = (function(mimeExtensions) {
const mimeTypes = { default: 'application/octet-stream' };
for (let mime in mimeExtensions) {
const extensions = mimeExtensions[mime];
for (let i = 0; i < extensions.length; i++) {
mimeTypes[extensions[i]] = mime;
}
}
return mimeTypes;
})({
// taken from https://github.com/broofa/mime/blob/main/types/standard.ts
'application/andrew-inset': ['ez'],
'application/appinstaller': ['appinstaller'],
'application/applixware': ['aw'],
'application/appx': ['appx'],
'application/appxbundle': ['appxbundle'],
'application/atom+xml': ['atom'],
'application/atomcat+xml': ['atomcat'],
'application/atomdeleted+xml': ['atomdeleted'],
'application/atomsvc+xml': ['atomsvc'],
'application/atsc-dwd+xml': ['dwd'],
'application/atsc-held+xml': ['held'],
'application/atsc-rsat+xml': ['rsat'],
'application/automationml-aml+xml': ['aml'],
'application/automationml-amlx+zip': ['amlx'],
'application/bdoc': ['bdoc'],
'application/calendar+xml': ['xcs'],
'application/ccxml+xml': ['ccxml'],
'application/cdfx+xml': ['cdfx'],
'application/cdmi-capability': ['cdmia'],
'application/cdmi-container': ['cdmic'],
'application/cdmi-domain': ['cdmid'],
'application/cdmi-object': ['cdmio'],
'application/cdmi-queue': ['cdmiq'],
'application/cpl+xml': ['cpl'],
'application/cu-seeme': ['cu'],
'application/cwl': ['cwl'],
'application/dash+xml': ['mpd'],
'application/dash-patch+xml': ['mpp'],
'application/davmount+xml': ['davmount'],
'application/dicom': ['dcm'],
'application/docbook+xml': ['dbk'],
'application/dssc+der': ['dssc'],
'application/dssc+xml': ['xdssc'],
'application/ecmascript': ['ecma'],
'application/emma+xml': ['emma'],
'application/emotionml+xml': ['emotionml'],
'application/epub+zip': ['epub'],
'application/exi': ['exi'],
'application/express': ['exp'],
'application/fdf': ['fdf'],
'application/fdt+xml': ['fdt'],
'application/font-tdpfr': ['pfr'],
'application/geo+json': ['geojson'],
'application/gml+xml': ['gml'],
'application/gpx+xml': ['gpx'],
'application/gxf': ['gxf'],
'application/gzip': ['gz'],
'application/hjson': ['hjson'],
'application/hyperstudio': ['stk'],
'application/inkml+xml': ['ink', 'inkml'],
'application/ipfix': ['ipfix'],
'application/its+xml': ['its'],
'application/java-archive': ['jar', 'war', 'ear'],
'application/java-serialized-object': ['ser'],
'application/java-vm': ['class'],
'application/javascript': ['*js'],
'application/json': ['json', 'map'],
'application/json5': ['json5'],
'application/jsonml+json': ['jsonml'],
'application/ld+json': ['jsonld'],
'application/lgr+xml': ['lgr'],
'application/lost+xml': ['lostxml'],
'application/mac-binhex40': ['hqx'],
'application/mac-compactpro': ['cpt'],
'application/mads+xml': ['mads'],
'application/manifest+json': ['webmanifest'],
'application/marc': ['mrc'],
'application/marcxml+xml': ['mrcx'],
'application/mathematica': ['ma', 'nb', 'mb'],
'application/mathml+xml': ['mathml'],
'application/mbox': ['mbox'],
'application/media-policy-dataset+xml': ['mpf'],
'application/mediaservercontrol+xml': ['mscml'],
'application/metalink+xml': ['metalink'],
'application/metalink4+xml': ['meta4'],
'application/mets+xml': ['mets'],
'application/mmt-aei+xml': ['maei'],
'application/mmt-usd+xml': ['musd'],
'application/mods+xml': ['mods'],
'application/mp21': ['m21', 'mp21'],
'application/mp4': ['*mp4', '*mpg4', 'mp4s', 'm4p'],
'application/msix': ['msix'],
'application/msixbundle': ['msixbundle'],
'application/msword': ['doc', 'dot'],
'application/mxf': ['mxf'],
'application/n-quads': ['nq'],
'application/n-triples': ['nt'],
'application/node': ['cjs'],
'application/octet-stream': [
'bin',
'dms',
'lrf',
'mar',
'so',
'dist',
'distz',
'pkg',
'bpk',
'dump',
'elc',
'deploy',
'exe',
'dll',
'deb',
'dmg',
'iso',
'img',
'msi',
'msp',
'msm',
'buffer',
],
'application/oda': ['oda'],
'application/oebps-package+xml': ['opf'],
'application/ogg': ['ogx'],
'application/omdoc+xml': ['omdoc'],
'application/onenote': [
'onetoc',
'onetoc2',
'onetmp',
'onepkg',
'one',
'onea',
],
'application/oxps': ['oxps'],
'application/p2p-overlay+xml': ['relo'],
'application/patch-ops-error+xml': ['xer'],
'application/pdf': ['pdf'],
'application/pgp-encrypted': ['pgp'],
'application/pgp-keys': ['asc'],
'application/pgp-signature': ['sig', '*asc'],
'application/pics-rules': ['prf'],
'application/pkcs10': ['p10'],
'application/pkcs7-mime': ['p7m', 'p7c'],
'application/pkcs7-signature': ['p7s'],
'application/pkcs8': ['p8'],
'application/pkix-attr-cert': ['ac'],
'application/pkix-cert': ['cer'],
'application/pkix-crl': ['crl'],
'application/pkix-pkipath': ['pkipath'],
'application/pkixcmp': ['pki'],
'application/pls+xml': ['pls'],
'application/postscript': ['ai', 'eps', 'ps'],
'application/provenance+xml': ['provx'],
'application/pskc+xml': ['pskcxml'],
'application/raml+yaml': ['raml'],
'application/rdf+xml': ['rdf', 'owl'],
'application/reginfo+xml': ['rif'],
'application/relax-ng-compact-syntax': ['rnc'],
'application/resource-lists+xml': ['rl'],
'application/resource-lists-diff+xml': ['rld'],
'application/rls-services+xml': ['rs'],
'application/route-apd+xml': ['rapd'],
'application/route-s-tsid+xml': ['sls'],
'application/route-usd+xml': ['rusd'],
'application/rpki-ghostbusters': ['gbr'],
'application/rpki-manifest': ['mft'],
'application/rpki-roa': ['roa'],
'application/rsd+xml': ['rsd'],
'application/rss+xml': ['rss'],
'application/rtf': ['rtf'],
'application/sbml+xml': ['sbml'],
'application/scvp-cv-request': ['scq'],
'application/scvp-cv-response': ['scs'],
'application/scvp-vp-request': ['spq'],
'application/scvp-vp-response': ['spp'],
'application/sdp': ['sdp'],
'application/senml+xml': ['senmlx'],
'application/sensml+xml': ['sensmlx'],
'application/set-payment-initiation': ['setpay'],
'application/set-registration-initiation': ['setreg'],
'application/shf+xml': ['shf'],
'application/sieve': ['siv', 'sieve'],
'application/smil+xml': ['smi', 'smil'],
'application/sparql-query': ['rq'],
'application/sparql-results+xml': ['srx'],
'application/sql': ['sql'],
'application/srgs': ['gram'],
'application/srgs+xml': ['grxml'],
'application/sru+xml': ['sru'],
'application/ssdl+xml': ['ssdl'],
'application/ssml+xml': ['ssml'],
'application/swid+xml': ['swidtag'],
'application/tei+xml': ['tei', 'teicorpus'],
'application/thraud+xml': ['tfi'],
'application/timestamped-data': ['tsd'],
'application/toml': ['toml'],
'application/trig': ['trig'],
'application/ttml+xml': ['ttml'],
'application/ubjson': ['ubj'],
'application/urc-ressheet+xml': ['rsheet'],
'application/urc-targetdesc+xml': ['td'],
'application/voicexml+xml': ['vxml'],
'application/wasm': ['wasm'],
'application/watcherinfo+xml': ['wif'],
'application/widget': ['wgt'],
'application/winhlp': ['hlp'],
'application/wsdl+xml': ['wsdl'],
'application/wspolicy+xml': ['wspolicy'],
'application/xaml+xml': ['xaml'],
'application/xcap-att+xml': ['xav'],
'application/xcap-caps+xml': ['xca'],
'application/xcap-diff+xml': ['xdf'],
'application/xcap-el+xml': ['xel'],
'application/xcap-ns+xml': ['xns'],
'application/xenc+xml': ['xenc'],
'application/xfdf': ['xfdf'],
'application/xhtml+xml': ['xhtml', 'xht'],
'application/xliff+xml': ['xlf'],
'application/xml': ['xml', 'xsl', 'xsd', 'rng'],
'application/xml-dtd': ['dtd'],
'application/xop+xml': ['xop'],
'application/xproc+xml': ['xpl'],
'application/xslt+xml': ['*xsl', 'xslt'],
'application/xspf+xml': ['xspf'],
'application/xv+xml': ['mxml', 'xhvml', 'xvml', 'xvm'],
'application/yang': ['yang'],
'application/yin+xml': ['yin'],
'application/zip': ['zip'],
'application/zip+dotlottie': ['lottie'],
'audio/3gpp': ['*3gpp'],
'audio/aac': ['adts', 'aac'],
'audio/adpcm': ['adp'],
'audio/amr': ['amr'],
'audio/basic': ['au', 'snd'],
'audio/midi': ['mid', 'midi', 'kar', 'rmi'],
'audio/mobile-xmf': ['mxmf'],
'audio/mp3': ['*mp3'],
'audio/mp4': ['m4a', 'mp4a', 'm4b'],
'audio/mpeg': ['mpga', 'mp2', 'mp2a', 'mp3', 'm2a', 'm3a'],
'audio/ogg': ['oga', 'ogg', 'spx', 'opus'],
'audio/s3m': ['s3m'],
'audio/silk': ['sil'],
'audio/wav': ['wav'],
'audio/wave': ['*wav'],
'audio/webm': ['weba'],
'audio/xm': ['xm'],
'font/collection': ['ttc'],
'font/otf': ['otf'],
'font/ttf': ['ttf'],
'font/woff': ['woff'],
'font/woff2': ['woff2'],
'image/aces': ['exr'],
'image/apng': ['apng'],
'image/avci': ['avci'],
'image/avcs': ['avcs'],
'image/avif': ['avif'],
'image/bmp': ['bmp', 'dib'],
'image/cgm': ['cgm'],
'image/dicom-rle': ['drle'],
'image/dpx': ['dpx'],
'image/emf': ['emf'],
'image/fits': ['fits'],
'image/g3fax': ['g3'],
'image/gif': ['gif'],
'image/heic': ['heic'],
'image/heic-sequence': ['heics'],
'image/heif': ['heif'],
'image/heif-sequence': ['heifs'],
'image/hej2k': ['hej2'],
'image/ief': ['ief'],
'image/jaii': ['jaii'],
'image/jais': ['jais'],
'image/jls': ['jls'],
'image/jp2': ['jp2', 'jpg2'],
'image/jpeg': ['jpg', 'jpeg', 'jpe'],
'image/jph': ['jph'],
'image/jphc': ['jhc'],
'image/jpm': ['jpm', 'jpgm'],
'image/jpx': ['jpx', 'jpf'],
'image/jxl': ['jxl'],
'image/jxr': ['jxr'],
'image/jxra': ['jxra'],
'image/jxrs': ['jxrs'],
'image/jxs': ['jxs'],
'image/jxsc': ['jxsc'],
'image/jxsi': ['jxsi'],
'image/jxss': ['jxss'],
'image/ktx': ['ktx'],
'image/ktx2': ['ktx2'],
'image/pjpeg': ['jfif'],
'image/png': ['png'],
'image/sgi': ['sgi'],
'image/svg+xml': ['svg', 'svgz'],
'image/t38': ['t38'],
'image/tiff': ['tif', 'tiff'],
'image/tiff-fx': ['tfx'],
'image/webp': ['webp'],
'image/wmf': ['wmf'],
'message/disposition-notification': ['disposition-notification'],
'message/global': ['u8msg'],
'message/global-delivery-status': ['u8dsn'],
'message/global-disposition-notification': ['u8mdn'],
'message/global-headers': ['u8hdr'],
'message/rfc822': ['eml', 'mime', 'mht', 'mhtml'],
'model/3mf': ['3mf'],
'model/gltf+json': ['gltf'],
'model/gltf-binary': ['glb'],
'model/iges': ['igs', 'iges'],
'model/jt': ['jt'],
'model/mesh': ['msh', 'mesh', 'silo'],
'model/mtl': ['mtl'],
'model/obj': ['obj'],
'model/prc': ['prc'],
'model/step': ['step', 'stp', 'stpnc', 'p21', '210'],
'model/step+xml': ['stpx'],
'model/step+zip': ['stpz'],
'model/step-xml+zip': ['stpxz'],
'model/stl': ['stl'],
'model/u3d': ['u3d'],
'model/vrml': ['wrl', 'vrml'],
'model/x3d+binary': ['*x3db', 'x3dbz'],
'model/x3d+fastinfoset': ['x3db'],
'model/x3d+vrml': ['*x3dv', 'x3dvz'],
'model/x3d+xml': ['x3d', 'x3dz'],
'model/x3d-vrml': ['x3dv'],
'text/cache-manifest': ['appcache', 'manifest'],
'text/calendar': ['ics', 'ifb'],
'text/coffeescript': ['coffee', 'litcoffee'],
'text/css': ['css'],
'text/csv': ['csv'],
'text/html': ['html', 'htm', 'shtml'],
'text/jade': ['jade'],
'text/javascript': ['js', 'mjs'],
'text/jsx': ['jsx'],
'text/less': ['less'],
'text/markdown': ['md', 'markdown'],
'text/mathml': ['mml'],
'text/mdx': ['mdx'],
'text/n3': ['n3'],
'text/plain': ['txt', 'text', 'conf', 'def', 'list', 'log', 'in', 'ini'],
'text/richtext': ['rtx'],
'text/rtf': ['*rtf'],
'text/sgml': ['sgml', 'sgm'],
'text/shex': ['shex'],
'text/slim': ['slim', 'slm'],
'text/spdx': ['spdx'],
'text/stylus': ['stylus', 'styl'],
'text/tab-separated-values': ['tsv'],
'text/troff': ['t', 'tr', 'roff', 'man', 'me', 'ms'],
'text/turtle': ['ttl'],
'text/uri-list': ['uri', 'uris', 'urls'],
'text/vcard': ['vcard'],
'text/vtt': ['vtt'],
'text/wgsl': ['wgsl'],
'text/xml': ['*xml'],
'text/yaml': ['yaml', 'yml'],
'video/3gpp': ['3gp', '3gpp'],
'video/3gpp2': ['3g2'],
'video/h261': ['h261'],
'video/h263': ['h263'],
'video/h264': ['h264'],
'video/iso.segment': ['m4s'],
'video/jpeg': ['jpgv'],
'video/jpm': ['*jpm', '*jpgm'],
'video/mj2': ['mj2', 'mjp2'],
'video/mp2t': ['ts', 'm2t', 'm2ts', 'mts'],
'video/mp4': ['mp4', 'mp4v', 'mpg4'],
'video/mpeg': ['mpeg', 'mpg', 'mpe', 'm1v', 'm2v'],
'video/ogg': ['ogv'],
'video/quicktime': ['qt', 'mov'],
'video/webm': ['webm'],
});
// HTTP status codes
const STATUS_CODES = {
200: 'OK',
302: 'Found',
404: 'Not Found',
};
// show help message and exit
function showHelp(exitCode) {
console.log('Syntax: node-serve [-h] [-d DIRECTORY] [PORT]');
console.log();
console.log('Positional arguments:');
console.log(' PORT port number (default: 8000)');
console.log();
console.log('Options:');
console.log(' -h, --help show this help message and exit');
console.log(' -d, --directory DIRECTORY serve this directory (default: current directory)');
process.exit(exitCode);
}
// show command line error message and exit
function syntaxError(message) {
console.log(message);
console.log();
showHelp(1);
}
// display runtime error message and exit
function runtimeError(err) {
console.log(`Error: ${err.message}`);
process.exit(err.errno);
}
// detect file existance and its type
async function getFileType(localPath) {
try {
let stats = await fs.promises.stat(localPath);
return stats.isFile() ? 'file'
: stats.isDirectory() ? 'directory'
: stats.isBlockDevice() ? 'block device'
: stats.isCharacterDevice() ? 'character device'
: stats.isFIFO() ? 'fifo'
: stats.isSocket() ? 'socket'
: stats.isSymbolicLink() ? 'symbolic link'
: 'unknown';
} catch (err) {
return err.code === 'ENOENT' ? 'not exist' : 'unknown error';
}
}
// check for a normal file
async function isFile(localPath) {
return (await getFileType(localPath)) === 'file';
}
// check for a directory
async function isDirectory(localPath) {
return (await getFileType(localPath)) === 'directory';
}
// Server settings with default values
let PORT = 8000; // port number
let DIRECTORY = process.cwd(); // local root directory
// command line parser
async function parseArgv() {
let argv = process.argv.slice(2);
while (argv.length > 0) {
let arg = argv.shift();
if (arg === '-h' || arg === '--help' || arg === '-?' || arg === '?') {
showHelp(0);
} else if (arg === '-d' || arg === '--directory') {
// parse DIRECTORY
if (argv.length === 0) {
syntaxError('Error (-d, --directory): DIRECTORY is not specified');
}
arg = argv.shift();
const dir = path.isAbsolute(arg) ? path.join(arg) : path.join(process.cwd(), arg);
if (await isDirectory(dir)) {
DIRECTORY= dir;
} else {
syntaxError(`Error (-d, --directory): ${arg} is not a directory`);
}
} else {
// parse PORT
const port = Number(arg);
if (Number.isNaN(port)) {
syntaxError(`Error: invalid option or argument: ${arg}`);
}
if (Number.isInteger(port) && port >= 0 && port < 65536) {
PORT = port;
}
if (argv.length > 0) {
syntaxError(`Error: unused argument/option: ${argv.join(' ')}`);
}
}
}
}
// analize URL and lookup local file/directory
async function lookupFile(url) {
let statusCode = 404; // defaults to 'Not Found'
let localPath = null; // used on 200 OK
let listing = false; // used on 200 OK
let location = null; // used on 302 Found
if (url.endsWith('/')) {
localPath = path.join(DIRECTORY, url);
if (await isDirectory(localPath)) {
statusCode = 200;
const index = path.join(localPath, 'index.html');
if (await isFile(index)) {
localPath = index;
} else {
listing = true;
}
}
} else {
localPath = path.join(DIRECTORY, url);
switch (await getFileType(localPath)) {
case 'file':
statusCode = 200;
break;
case 'directory':
statusCode = 302;
location = `${encodeURI(url)}/`;
break;
case 'not exist':
localPath = `${localPath}.html`;
if (await isFile(localPath)) {
statusCode = 200;
}
break;
}
}
return { statusCode, localPath, listing, location };
}
// Serve directory listing
async function serveDirectory(res, url, localPath) {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.write("<!DOCTYPE HTML>\n");
res.write("<html lang=\"en\">\n");
res.write("<head>\n");
res.write("<meta charset=\"utf-8\">\n");
res.write(`<title>Directory listing for ${url}</title>\n`);
res.write("<body>\n");
res.write(`<h1>Directory listing for ${url}</h1>\n`);
res.write("<hr>\n");
res.write("<ul>\n");
try {
for (const file of await fs.promises.readdir(localPath)) {
res.write(`<li><a href="${file}">${file}</a></li>`);
}
} catch (err) {
res.write(`<p>Error: ${err}</p>\n`);
}
res.write("</ul>\n");
res.write("<hr>\n");
res.write("</body>\n");
res.end("</html>\n");
}
// Serve file
function serveFile(res, localPath) {
const ext = path.extname(localPath).slice(1).toLowerCase();
let mimeType = MIME_TYPES[ext] || MIME_TYPES.default;
if (mimeType.startsWith('text/')) {
mimeType += '; charset=utf-8';
}
res.writeHead(200, { 'Content-Type': mimeType });
fs.createReadStream(localPath).pipe(res);
}
// HTTP request handler
async function handleRequest(req, res) {
const url = decodeURI(req.url).replace(/\?.*$/, '');
const { statusCode, localPath, listing, location } = await lookupFile(url);
const message = `${statusCode} ${STATUS_CODES[statusCode]}`;
switch (statusCode) {
case 200:
if (listing) {
await serveDirectory(res, url, localPath);
} else {
serveFile(res, localPath);
}
break;
case 302:
res.writeHead(302, { 'Content-Type': 'text/plain', 'Location': location });
res.end(message);
break;
case 404:
default:
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end(message);
break;
}
console.log(`${(new Date).toLocaleString()} ${req.method} ${req.url} → ${message}`);
}
// MAIN
// ----
await parseArgv();
try {
const server = http.createServer(handleRequest);
server.on('error', function(err) { runtimeError(err) });
server.listen(PORT);
console.log('Serving HTTP (Ctrl-C to exit)');
console.log('────────────────────────────────────');
console.log(`- local: http://localhost:${PORT}`);
console.log(`- Network: http://127.0.0.1:${PORT}`);
console.log('────────────────────────────────────');
} catch (err) {
runtimeError(err);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment