Server-Side JavaScript Arc Part 2: Oden, a Server-First JavaScript Runtime
Be sure to first read part one, A Retrospective on the Golden Age of Node.js, for context. It discusses the rise of Node.js and how it gained a lot of complexity by incorporating browser functionality for sake of script compatibility.
Regardless of how helpful a JavaScript API is when provided in Node.js it is usually exposed in a universal-JavaScript compatible way. For example the URL class is available as global.URL (both in Node.js and in a Browser) and is always in scope. Even if your program doesn't touch HTTP, that global is still there in memory. Node.js itself of course keeps different Node.js APIs locked up until needed by hiding them behind require() statements. URL is also available as require('url').URL after all. As another example the original way of making an HTTP request requires you to first require('http'):
const http = require('http');
http.request(...);
In the case of fetch, while I find it useful in my applications, I would have personally preferred to see it hidden behind a require. Imagine these two approaches:
const { fetch } = require('http');
const fetch = require('node:fetch');
Requiring an explicit require() before accessing such APIs makes it easier to audit application code and find out what modules are talking to the network. Theoretically there would be performance benefits as well.
Of course, Node.js could have taken this approach. Node.js uses the undici library under the hood at the fetch implementation. Code exists to load it and make it available as a global, but it could have easily been locked away behind a require. I suppose we then need to pose the question: if fetch isn't available as global.fetch then are we even writing JavaScript?
There are of course alternative server-side JavaScript runtimes out there, the most popular of them being Deno and Bun, and even more esoteric ones like workerd. Deno's design goal has been to embrace web APIs entirely from the start. It supports things with no server equivalent at all like window.close(), alert(),
A White Room Implementation
What would a white room implementation of a JavaScript server runtime look like? One that isn't beholden to vestigial APIs intended for browsers?
Such an implementation should hide most features behind import / require statements. Even things that we take for granted. Instead of setTimeout, it could be available as require('timers').setTimeout (even in Node.js, global.setTimeout === require('timers').setTimeout). This cleans up the global namespace and makes code audits easier.
Another big change would be to not implement browser-specific APIs at all. Did you know that "x".sup() === "<sup>x</sup>"? There's a bunch of methods on String.prototype that are hanging around from early browser days. Methods that are obviously silly today, even in a browser. Why does a String need to know about HTML? To go further, there's a String.prototype.bold() method but there's no String.prototype.strong() method. We realized at some point that it was untenable to keep adding string methods for each new HTML tag.
Programming languages are interesting because they are often two concepts in one. For example, Python is both a syntax and a binary to interpret .py files. Ruby and PHP are similar. Node.js is special in that it isn't it's own language and doesn't invent new language syntax that would benefit its interpreter; Node.js depends almost entirely on upstream changes landing in V8 (of course, Node.js TSC members do influence the direction of the JavaScript language).
If a white-room, server-first runtime is going to such lengths to break compatibility with browser code then why use plain JavaScript? Why not fork JavaScript and bend it to our will? Just imagine having a special syntax for common Node.js features like EventEmitter events.
I have a work-in-progress RFC detailing these changes available here:
Oden.js
Here's a silly thing I made in early 2023. It's mostly gathered dust though I did modernize it a bit during the 2024 Node.js collaborators summit. It's a single script that you run via node --require=./oden.js. From there you can experiment in the REPL. At a high level the script removes many globally-accessed JavaScript features, in some cases locking them behind a require() call (like timers), and in other cases removing them entirely (like string HTML methods).
Essentially Oden.js is a pile of hacks to implement the shallowest layer of changes described in this blog post. Oden.js is a way to get a feel for what Oden could be.
If you press the <tab> key you'll notice that nearly all of the globals are missing. It is obviously not the "JavaScript replacement" as alluded to above since it's all userland tweaks but it does start to show how such a JavaScript fork could look.


Many other niceties have been added. Stringifying Object, Set, and Map instances return JSON strings. Stringifying Date instances return ISO8601 values.
Of course, this approach doesn't modify V8 and therefor isn't able to add new syntax. Imagine finally be able to change typeof null === 'null'!
Here's an example of an Oden.js script:
const Math = require('math');
const setTimeout = require('timers').setTimeout;
const { Date } = require('datetime');
let m = new Map();
m.set('foo', 'bar');
let s = new Set(['a', 'b']);
let o = {x:'y'};
let d = new Date();
console.log(String(m)); // '{"foo":"bar"}'
console.log(String(s)); // '["a","b"]'
console.log(String(o)); // '{"x":"y"}'
console.log(String(d)); // '2026-04-22T23:19:09.256Z'
console.log(d.toISOTimezoneString()); // '2026-04-22T16:19:09-07:00'
The code for this project is available in the tlhunter/Oden.js gist. Download it and require it in your REPL to get a feel for how it works. The gist is embedded here if you'd like to see how it works: