Building a Local Server for Static Site Development from Scratch
Introduction
A local development server is an essential tool for web development that allows you to:
Instantly preview changes in the browser
Mimic production hosting rules
Automate routine operations
Why build from scratch?
While these features come built-in with modern tools, building them yourself provides valuable insights into how core systems operate.
Previous Material:
If you missed the introductory article, check out how I built a static site generator from scratch using NodeJS!
Core System Features:
- HTTP Server serves generated pages and static assets using hosting-like rules
- File Watcher monitors changes in source files and generator code
- Rebuild Process triggers site regeneration on changes
- EventStream sends reload signals to all open browser tabs
Monitoring File Changes
Initially used NodeJS's native fs.watch
, but switched to chokidar for better cross-platform support. I was just tired of duplicated file change notifications.
Interesting? Maybe you're interested in a system that builds an index and tracks real file content changes? So the generator only runs when the file content has actually changed? If so, please let me know!
Site Regeneration
I launch the regeneration process in a separate thread. The development build includes a special script injection for live reloading.
A debounce
function prevents multiple regeneration triggers:
export const debounce = (fn, ms) => {
let timeoutId = null;
return (...args) => {
if (timeoutId != null) {
clearTimeout(timeoutId);
timeoutId = null;
}
return new Promise((resolve) => {
timeoutId = setTimeout(() => {
timeoutId = null;
const result = fn(...args);
resolve(result);
}, ms);
});
};
};
If a previous build is running, abort it before starting a new one:
const proc = runProcess("node", ["--import", "tsx", "scripts/dev-rebuild.ts"]);
// After this, I can:
// 1. Wait for process completion
await proc.promise;
// 2. Abort and terminate the process (proc.promise will reject)
await proc.abort();
Local HTTP Server
Create a custom HTTP server (e.g., at http://localhost:3000/
) to serve files from the build directory.
Path Handling Logic
Server routing mimics production environment rules:
/
→/index.html
/about
→/about.html
/about/
→/about/index.html
Development Mode & Page Reloading
Implement EventStream connections for live reloading. Inject this script into every HTML page:
// Connect to dev server at http://localhost:3000/dev-server
new EventSource("/dev-server").addEventListener("message", (ev) => {
if (ev?.data === "reload") {
location.reload();
}
});
Pro Tip: Inject the script dynamically during file serving rather than at build time. This keeps static pages unaware of the reload mechanism.
Next, handle event stream on local server, store all active connections just in memory and broadcast reload event:
// Store all active connections
const clients = new Set();
http.createServer(async (req, res) => {
if (req.url === "/dev-server") {
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
});
res.write("data: init\n\n");
// Add new connection
clients.add(res);
req.on("close", () => {
clients.delete(res);
res.end();
});
return;
}
// ...
});
export const reloadPages = () => {
for (const client of clients) {
client.write("data: reload\n\n");
}
};
Conclusion
My system now can:
Automatically rebuild pages
Sync changes across browser tabs
Work faster with
debounce
optimization
Your feedback matters!
Let me know, did you find this article helpful?
What topic should I explore next?
Follow me on social media to stay updated!