cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

Beyond the browser: Node.js, Deno & Bun

Learn how Node.js, Deno, Bun, and edge runtimes run JavaScript beyond browsers, from files and processes to shared web APIs.

By the end, you can
  • 01
    Separate language from runtimeExplain what Node.js, Deno, Bun, and edge hosts add around JavaScript engines.
  • 02
    Read server-side examplesRecognize files, paths, streams, processes, exit codes, and module-system choices in Node code.
  • 03
    Write portable handlersUse shared web APIs such as Request, Response, URL, streams, and Web Crypto when code should move across runtimes.

Beyond the browser

Earlier lessons separated the JavaScript language from the host that runs it. In Engines, runtimes & hosts, you learned that V8, SpiderMonkey, and JavaScriptCore run language features while a host provides APIs. In Runtime overview, you connected engines, host APIs, queues, and the event loop. This lesson builds on that map and asks a professional question: what changes when JavaScript leaves a browser page?

Definition

A JavaScript runtime is a program that embeds an engine and adds host capabilities. Browser runtimes add the DOM. Node.js adds files, processes, core modules, and npm. Deno and Bun add their own tools. Edge runtimes add a fetch-handler model close to users.

Real-life analogySame musician, different venues

The song can be the same, but the venue decides what equipment is available. JavaScript is the song; the runtime is the venue.

In real life: The musician knows the song
In JavaScript: The engine understands JavaScript syntax and built-ins
In real life: A studio has microphones and file storage
In JavaScript: Node has fs, process, child processes, and packages
In real life: A concert hall has audience lights and seats
In JavaScript: A browser has window, document, and user events
In real life: A pop-up stage has a tiny setup near the crowd
In JavaScript: An edge runtime runs a small request handler near users

Where the analogy stops: The musician can improvise around missing equipment. JavaScript cannot: if the host does not provide fs, document, or a platform binding, code using that name fails.

Browser, server, tool, and edge runtimes at a glance.
RuntimeEngine or shapeHost APIsGood fit
BrowserV8, SpiderMonkey, or JavaScriptCore inside a pageDOM, window, storage, user events, Web APIsInteractive websites and apps
Node.jsV8 plus libuv, built-in modules, and npmfs, path, os, process, child_process, http, streams, cryptoCLIs, servers, build tools, scripts
DenoV8 with secure-by-default permissionsWeb APIs, built-in TypeScript, deno.json, npm: and jsr: specifiersSecure scripts, TypeScript tools, web-aligned services
BunJavaScriptCore with an all-in-one toolchainRuntime, package manager, bundler, test runner, Node compatibility workFast local tooling and Node-compatible apps
Edge runtimeUsually V8 isolates or a Deno-based isolate modelFetch handler, web APIs, platform bindings, no local fsLow-latency personalization, redirects, API edges

Keep one rule in mind: Node-only code is not browser code. The lesson marks Node, Deno, Bun, and shell examples as non-runnable in the page. The tests run the Node snippets with real Node instead of pretending the browser can do it.

What Node.js adds to JavaScript

NODE HOST

Node.js is not a different language. It is JavaScript running in a server and tooling runtime: V8 runs the language, libuv helps with evented I/O, built-in modules expose operating-system features, and the npm ecosystem supplies packages. The upcoming npm & package.json lesson goes deeper into package installation and scripts.

Plain Node does not give you window, document, or DOM elements. Use globalThis for the global object name that also works in browsers and modern server runtimes. Use process for Node-specific process information.

Core Node modules used constantly in tools and servers.
ModuleWhat it adds
fs / fs/promisesRead and write files. Use promise APIs for ordinary async scripts.
pathBuild file paths with the right separator for the operating system.
osRead platform information such as CPU, home directory, and temp directory details.
processRead argv, env, current working directory, and set exitCode.
child_processSpawn another program and inspect its exit status.
httpCreate lower-level HTTP servers and clients. Many frameworks build on it.
streamProcess large data gradually instead of loading it all into memory.
cryptoHash, sign, encrypt, and generate secure IDs; Node also exposes Web Crypto.

This Node-only script reads command-line data, writes and reads a file, joins paths safely, inspects the OS, and sets process.exitCode. It is marked non-runnable because the in-page editor is a browser sandbox, not Node.

Node host APIs: process, fs/promises, path, osJavaScript
import { readFile, writeFile } from "node:fs/promises";import path from "node:path";import os from "node:os"; process.env.RUNTIME_LESSON = "Node.js";const name = process.argv[1] ?? "friend";const file = path.join(process.cwd(), "message.txt"); await writeFile(file, `Hello ${name} from ${process.env.RUNTIME_LESSON}`);const text = await readFile(file, "utf8");process.exitCode = text.includes(name) ? 0 : 1; console.log(path.basename(file));console.log(text);console.log(os.platform().length > 0);

Line 1 imports promise-based file helpers. Line 2 uses path.join so the script works on different operating systems. Line 6 reads the first user argument from process.argv. Line 11 sets the exit code but lets the script finish naturally.

ES modules and CommonJS side by sideJavaScript
// ECMAScript moduleimport { readFile } from "node:fs/promises";export async function loadConfig(file) {  return JSON.parse(await readFile(file, "utf8"));} // CommonJS moduleconst { readFileSync } = require("node:fs");module.exports.loadConfigSync = (file) => {  return JSON.parse(readFileSync(file, "utf8"));};

Modern Node supports ECMAScript modules with import and export, and it still supports CommonJS with require and module.exports. The CommonJS & ESM lesson covers package settings, extensions, and migration details.

Files, streams, child processes, and exit codes

STEP THROUGH

Runtime code often starts as a command-line tool. Node gives a script an argument array, environment variables, a current working directory, and a way to report success or failure. The parser below deliberately avoids Node APIs so you can step through it in the browser, then reuse the same logic with process.argv.slice(2) in Node.

CLI argument parser lab
Step 0 of 9Ready
Your turn: follow the blue line

Step through a browser-safe parser for Node-style command-line arguments. The parser is pure; Node only supplies the argv array around it.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
  const options = { _: [] };  for (const token of argv) {    if (token.startsWith("--")) {      const [key, value = "true"] = token.slice(2).split("=");      options[key] = value;    } else {      options._.push(token);    }  }  return options;} const parsed = parseArgs(["build", "--watch", "--out=dist"]);console.log(JSON.stringify(parsed));
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
Choose a simulated `process.argv.slice(2)`

Changing the sample starts a fresh replay. The parser stays browser-safe; Node supplies the real `argv` array around it.

A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.
Try the CLI parser with different arguments
CLI parser sourcePop out in the code editor (opens in a new tab)JavaScript
function parseArgs(argv) {  const options = { _: [] };  for (const token of argv) {    if (token.startsWith("--")) {      const [key, value = "true"] = token.slice(2).split("=");      options[key] = value;    } else {      options._.push(token);    }  }  return options;} const parsed = parseArgs(["build", "--watch", "--out=dist"]);console.log(JSON.stringify(parsed));
OutputBuild command
argv slice["build","--watch","--out=dist"]
printed JSON{"_":["build"],"watch":"true","out":"dist"}
Try it yourself

The same parsing function can be unit-tested in a browser, then used with process.argv.slice(2) inside a real Node CLI.

The output is produced by the same parseCliArgs function used in the step-through lab.

File APIs are fine for small config files, but large logs, uploads, and responses should be streamed. Streams let a program process chunks gradually instead of loading everything into memory at once. The Streams & progress lesson teaches browser streams in depth; Node also has classic streams and web streams.

Node streams for a file you do not want to load all at onceJavaScript
import { createReadStream } from "node:fs";import { writeFile } from "node:fs/promises";import path from "node:path"; const file = path.join(process.cwd(), "access.log");await writeFile(file, "GET /a\nGET /b\nGET /c\n"); let chunks = 0;let characters = 0;for await (const chunk of createReadStream(file, { encoding: "utf8", highWaterMark: 8 })) {  chunks += 1;  characters += chunk.length;} console.log(`${chunks} chunks`);console.log(`${characters} characters`);

Line 10 starts reading the file in small chunks. The loop body updates counters for each chunk. In production, the body might parse a CSV row, update a hash, or stream data to an HTTP response.

Node child process, crypto, and httpJavaScript
import { spawnSync } from "node:child_process";import { createHash } from "node:crypto";import http from "node:http"; const digest = createHash("sha256").update("runtimes").digest("hex").slice(0, 8);const child = spawnSync(process.execPath, ["-e", "process.exitCode = 7"]);const server = http.createServer((request, response) => {  response.end(`ok ${request.url}`);}); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));const address = server.address();const body = await new Promise((resolve, reject) => {  http.get({ host: "127.0.0.1", port: address.port, path: "/health" }, (response) => {    let data = "";    response.setEncoding("utf8");    response.on("data", (chunk) => { data += chunk; });    response.on("end", () => resolve(data));  }).on("error", reject);});await new Promise((resolve) => server.close(resolve)); console.log(digest);console.log(child.status);console.log(body);

Line 6 starts a child Node process and reads its exit status. Lines 7 through 21 create a tiny HTTP server and make one request to it. Line 5 uses classic Node crypto; the common API section compares that with Web Crypto.

Deno & Bun: different trade-offs outside the browser

TOOLS

Deno and Bun are not installed in this workspace, so the lesson does not display fake command output or benchmark numbers. The commands below are shown as reference shapes and are verified against current official documentation in the lesson research.

Deno: secure by default and web-aligned

Deno uses V8, starts with sensitive permissions disabled, runs TypeScript without a separate project setup, supports web APIs, can import npm packages with npm: specifiers, and uses deno.json or deno.jsonc for project tasks and settings. File, network, environment, and subprocess access require explicit permission flags such as --allow-read or --allow-net.

Deno permission shapesBash
deno run main.tsdeno run --allow-read=./data main.tsdeno run --allow-net=api.example.com server.tsdeno run -A trusted-tool.ts
deno.json with tasks, JSR, npm, and TypeScript optionsJSON
{  "tasks": {    "start": "run --allow-read=./data --allow-net=api.example.com main.ts"  },  "imports": {    "@std/path": "jsr:@std/path",    "chalk": "npm:chalk@^5.0.0"  },  "compilerOptions": {    "strict": true  }}

Bun: JavaScriptCore and an all-in-one toolchain

Bun uses JavaScriptCore, the same JavaScript engine family as Safari. Its project is an all-in-one runtime, package manager, bundler, and test runner with ongoing Node compatibility work. Use this fact, not copied benchmark claims, when deciding whether Bun is a good fit for a team.

Bun command shapesBash
bun run index.tsbun testbun build ./src/app.ts --outdir=distbun add zod
Deno vs Bun in one sentence

Deno emphasizes secure permissions and web-standard TypeScript workflows; Bun emphasizes a fast, integrated JavaScriptCore-based toolchain with Node compatibility goals.

Edge runtimes: fetch handlers in isolates

REQUESTS

Edge platforms such as Cloudflare Workers, Vercel Edge Functions, Netlify Edge Functions, and Deno Deploy run code near users. Instead of a long-lived local process with full file access, they usually run small request handlers in isolate-style environments. The handler receives a Request and returns a Response.

Fetch-style handler lab
Step 0 of 10Ready
Your turn: follow the blue line

Step through the web-standard request/response shape. The same handler can be called by the page, by Node tests, or by an edge runtime.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
  const url = new URL(request.url);  if (url.pathname === "/api/greet") {    const name = url.searchParams.get("name") || "friend";    return Response.json({ message: `Hello ${name}` }, {      headers: { "x-runtime": "web-compatible" },    });  }  return new Response("Not found", { status: 404 });} const request = new Request("https://lesson.test/api/greet?name=Ada");const response = handleRequest(request);console.log(response.status);console.log(response.headers.get("x-runtime"));
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
Choose the request the runtime passes in

Edge platforms call a handler with a real `Request`; the lesson calls the same function from the page.

A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.
Call the handler with real Request objects
Fetch-style handlerPop out in the code editor (opens in a new tab)JavaScript
function handleRequest(request) {  const url = new URL(request.url);  if (url.pathname === "/api/greet") {    const name = url.searchParams.get("name") || "friend";    return Response.json({ message: `Hello ${name}` }, {      headers: { "x-runtime": "web-compatible" },    });  }  return new Response("Not found", { status: 404 });} const request = new Request("https://lesson.test/api/greet?name=Ada");const response = handleRequest(request);console.log(response.status);console.log(response.headers.get("x-runtime"));
ResponseGreeting Ada
request URLhttps://lesson.test/api/greet?name=Ada
statusnot run
x-runtimenot run
bodynot run
Try it yourself

This is the same (request) => Response shape used by many edge runtimes. No DOM, no window, and no local fs are needed.

Click Call handler. The page creates a real Request, receives a real Response, then reads its body.

Edge code is intentionally constrained. You should expect CPU and memory limits, no local file system, no child processes, and no full Node core-module surface. Exact limits vary by platform and plan, so production code should keep work short, stream when possible, and use platform storage bindings instead of local disk.

A platform fetch entry pointJavaScript
export default {  fetch(request, env, ctx) {    return handleRequest(request);  },}; function handleRequest(request) {  const country = request.headers.get("cf-ipcountry") || "unknown";  return new Response(`Hello ${country}`, {    headers: { "content-type": "text/plain" },  });}

This entry point looks like Cloudflare Worker style code: the platform calls fetch, your code returns a Response, and platform-specific data arrives through request headers or bindings such as env. The pure handler above keeps business logic portable.

Common web-interoperable APIs

PORTABLE

Modern runtimes share more APIs than they used to. The portable surface includes fetch, Request, Response, Headers, URL, URLSearchParams, Web Streams, TextEncoder, Web Crypto APIs such as crypto.subtle and crypto.randomUUID, structuredClone, AbortController, setTimeout, and EventTarget.

The standards effort is now called WinterTC, Ecma Technical Committee 55. It was formerly the WinterCG community group. Its Minimum Common API work has an Ecma standard snapshot, ECMA-429, so server runtimes have a concrete web-compatible baseline to aim at.

Common API probe
Common web-interoperable APIsPop out in the code editor (opens in a new tab)JavaScript
const request = new Request("https://lesson.test/search?q=edge");const url = new URL(request.url);const bytes = new TextEncoder().encode(url.searchParams.get("q"));const copy = structuredClone({ q: url.searchParams.get("q"), bytes: bytes.length });const response = new Response("ok", { headers: new Headers({ "x-api": "fetch" }) });const stream = response.body;const controller = new AbortController();const target = new EventTarget();let heard = false;target.addEventListener("done", () => { heard = true; });target.dispatchEvent(new Event("done")); console.log(`${copy.q}:${copy.bytes}:${response.headers.get("x-api")}`);console.log(stream instanceof ReadableStream);console.log(typeof crypto.subtle?.digest);console.log(crypto.randomUUID().length === 36);console.log(controller.signal.aborted);console.log(heard && typeof setTimeout === "function" && typeof fetch === "function");
Outputcurrent page
  1. edge:4:fetch
  2. true
  3. function
  4. true
  5. false
  6. true
Try it yourself
Real browser values

The code uses APIs that modern browsers, Node 22, Deno, Bun, and common edge runtimes intentionally share. The lesson tests run the same snippet in Node and Chrome.

The UUID value is random, so the snippet logs whether it has the standard 36-character shape instead of printing the ID.
Which runtimes provide this API?
  • fetch, Request, Response, Headers
  • URL and URLSearchParams
  • ReadableStream and TextEncoder
  • crypto.subtle and crypto.randomUUID
  • import { readFile } from 'node:fs/promises'
  • spawnSync(process.execPath, ['-e', code])
  • document.querySelector('button')
  • env.MY_KV.get('theme')
Try it yourself
0 of 8 correct

Sort each API family by whether it is common web surface, Node core, browser page API, or edge-platform-specific.

Choose a category for every card. You can change an answer at any time; Reset clears them all.
Portable does not mean identical

Shared APIs still have host differences: networking permissions, cookies, TLS stores, stream adapters, and platform limits can vary. Write tests in the runtime where you will deploy, and keep Node-only imports away from code meant for browsers or edge handlers.

Where you’ll use runtime knowledge

Professional JavaScript often crosses runtime boundaries. Build tools and CLIs run in Node, Deno, or Bun. Web apps run in browsers. API routes may run in Node serverless functions or edge isolates. Packages may publish one entry point for browsers and another for Node.

  • CLIs: read process.argv, validate flags, set process.exitCode, and stream output.
  • Build tools: read project files, spawn compilers, hash assets, and write bundles.
  • APIs: use Node http or framework adapters for long-running server code.
  • Edge personalization: inspect a Request, read a platform binding, and return a small Response quickly.
  • Portable utilities: prefer URL, Request, Web Streams, and Web Crypto when the code should travel.
A quick runtime checklist

Before importing a package or writing an API call, ask: does this code need the DOM, a Node core module, a permission flag, an edge binding, or only common web APIs?

Common misconceptions

  • “JavaScript outside the browser can use the DOM.” The DOM is a browser page API. Server runtimes can parse HTML with libraries, but they do not have a real page by default.
  • “Node built-ins are standard JavaScript.” Names like fs, path, and child_process are Node host APIs.
  • “Deno and Bun are just Node with new commands.” They make different choices around permissions, engines, tooling, and compatibility.
  • “Edge means unlimited server close to the user.” Edge code is close to users but constrained by CPU, memory, API, and platform limits.
  • “If an API is web-compatible, behavior is identical everywhere.” The shape travels, but host policies and limits still matter.
Similar runtime ideas, different decisions.
QuestionBrowserNodeDenoBunEdge
Can I read local files directly?No, not ordinary pagesYes with fsOnly with permissionServer/tooling yesUsually no local fs
Do I have window and document?YesNoNoNo real page by defaultNo real page
What handler shape is common?Event listeners and UI callbacksCLI entry or server callbackScript, task, or server handlerScript, test, build, or serverfetch(request) => Response
Best portability betWeb APIsCommon APIs plus adaptersWeb APIs and permissionsWeb APIs plus Node compatibilityMinimum Common API

Practice exercises

5 EXERCISES
Exercise 1 · Warm-upPredict CLI parser output

Read the pure parser and type the exact JSON string it prints.

Starter codePop out in the code editor (opens in a new tab)JavaScript
function parseArgs(argv) {
  const options = { _: [] };
  for (const token of argv) {
    if (token.startsWith("--")) {
      const [key, value = "true"] = token.slice(2).split("=");
      options[key] = value;
    } else {
      options._.push(token);
    }
  }
  return options;
}
console.log(JSON.stringify(parseArgs(["serve", "--port=3000", "--watch"])));

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · PracticeClassify Request

    Which API family should you choose for Request if code should run in browsers, Node 22, Deno, Bun, and edge runtimes?

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticePredict a handler status

      Predict the status printed by the fetch-style handler.

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      function handleRequest(request) {
        const url = new URL(request.url);
        if (url.pathname === "/api/greet") {
          return new Response("hello", { status: 200 });
        }
        return new Response("missing", { status: 404 });
      }
      console.log(handleRequest(new Request("https://lesson.test/nope")).status);

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeReport CLI failure without cutting off cleanup

        A Node CLI validated its inputs, printed a helpful error, and still needs cleanup callbacks to run. Which property should it set to report failure?

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeChoose a runtime for edge personalization

          A site wants to read request headers and return a small personalized response close to users. It does not need local files. Which runtime style is the best fit?

          Answer, then press Check. Spacing and letter case don’t matter.

            Check your understanding

            8 QUESTIONS
            Lesson quiz · 8 questionsScore: first tries count
            1. Question 1 of 8What does Node.js add around the JavaScript language?

              Choose an answer to see the explanation.

            2. Question 2 of 8What does this CLI parser print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              function parseArgs(argv) {
                const options = { _: [] };
                for (const token of argv) {
                  if (token.startsWith("--")) {
                    const [key, value = "true"] = token.slice(2).split("=");
                    options[key] = value;
                  } else {
                    options._.push(token);
                  }
                }
                return options;
              }
              console.log(JSON.stringify(parseArgs(["serve", "--port=3000", "--watch"])));

              Choose an answer to see the explanation.

            3. Question 3 of 8Which line is true about Node-only code in this lesson?

              Choose an answer to see the explanation.

            4. Question 4 of 8What is the practical difference between ESM and CommonJS in Node?

              Choose an answer to see the explanation.

            5. Question 5 of 8Which Deno command grants file-read permission to a script?

              Choose an answer to see the explanation.

            6. Question 6 of 8Why do edge runtimes often reject Node file-system code?

              Choose an answer to see the explanation.

            7. Question 7 of 8Which set belongs to the common web-interoperable API surface?

              Choose an answer to see the explanation.

            8. Question 8 of 8Which statement about Bun is accurate without relying on benchmark numbers?

              Choose an answer to see the explanation.

            Key takeaways

            • Node.js is V8 plus libuv, built-in modules, process access, and npm ecosystem tooling; it is not a browser page.
            • Use process.argv, process.env, and process.exitCode for CLI behavior, and streams for large data.
            • Deno emphasizes secure permissions, built-in TypeScript, web APIs, npm: imports, and deno.json.
            • Bun uses JavaScriptCore and combines runtime, package manager, bundler, and test runner ideas with Node compatibility work.
            • Edge runtimes favor small Request to Response handlers, web APIs, platform bindings, and strict limits.
            • The common web API surface is the best starting point for code that should move between browsers, Node, Deno, Bun, and edge.

            Remember the one-liner.
            The JavaScript language travels widely; runtime APIs decide what your code can touch.

            Up next: Unit testing.

            CompleteFrontend Clear concepts. Working examples.