completefrontend
Open lab My progress
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
    Tell the language from the runtimeSay which extra tools Node.js, Deno, Bun, and edge runtimes add around the engine that runs JavaScript.
  • 02
    Read Node.js codeSpot command words, files, paths, streams, other programs, exit codes, and the two module systems in Node.js code.
  • 03
    Write code that runs anywhereUse shared web APIs such as Request, Response, URL, streams, and Web Crypto in code that should run in many runtimes.

JavaScript outside the browser

So far, most of your JavaScript has run in a browser page. But JavaScript also runs on servers, in command-line tools, and on servers close to the people who use a website. This lesson shows what changes when JavaScript leaves the browser page.

Two earlier lessons set this up. In Engines, runtimes & hosts, you met the engine: the part that runs the JavaScript language. Chrome's engine is called V8. In Runtime overview, you saw how the engine, the host, and the event loop work together.

Definition

A JavaScript runtime is a program that runs JavaScript code. Inside it is an engine, which runs the language. Around the engine, the runtime adds extra tools called host APIs. An API is a set of names your code can call.

  • A browser adds the page, through document.
  • Node.js adds files, facts about the running program (process), and packages from npm.
  • Deno and Bun add their own tools.
  • An edge runtime adds a way to answer web requests on servers close to users.
Real-life analogySame musician, different venues

A musician can play the same song in many places. Each place has different equipment. JavaScript is the song. The runtime is the place, and it decides which equipment your code can use.

In real life: The musician knows the song
In JavaScript: The engine knows the JavaScript language
In real life: A recording studio has microphones and a place to store recordings
In JavaScript: Node.js has files (fs), the running program (process), and packages
In real life: A concert hall has seats and lights for an audience
In JavaScript: A browser has window, document, and clicks from the user
In real life: A small stage near the crowd has only a few tools
In JavaScript: An edge runtime runs one small request handler near users

The table compares five runtimes. Read one row at a time: what runs the code, which extra tools it adds, and what people use it for.

Five places where JavaScript runs.
RuntimeWhat runs the code?Which extra tools does it add?What do people use it for?
BrowserAn engine inside the browser: V8, SpiderMonkey, or JavaScriptCore.It adds the page (document), window, storage, and user events.Websites and web apps.
Node.jsThe V8 engine, with libuv to wait for files and the network.It adds fs, path, os, process, child_process, http, streams, crypto, and npm packages.Command-line tools, servers, build tools, and scripts.
DenoThe V8 engine, with no access to files or the network until you allow it.It adds web APIs, TypeScript with no setup, deno.json, and npm: and jsr: imports.Safe scripts, TypeScript tools, and web servers.
BunThe JavaScriptCore engine, from Safari.It adds a package manager, a bundler, and a test runner, and it aims to run Node.js code.Local tools and apps written for Node.js.
Edge runtimeUsually V8, in small sealed-off spaces called isolates.It adds a request handler, web APIs, and platform storage, but no local fs.Fast answers near users, such as redirects or a page made for each visitor.

Each runtime has a different set of global names. A global is a name you can use anywhere without importing it. A browser page has window, but plain Node.js does not. To check for a name that may be missing, use typeof. It gives the type of a value as a string. For a missing name, it gives "undefined" instead of throwing an error.

Check which global names exist herePop out in the code editor (opens in a new tab)JavaScript
console.log(typeof window);
console.log(typeof process);
console.log(typeof globalThis);

In a browser page, line 1 prints object and line 2 prints undefined. Node.js prints them the other way round: undefined, then object. Line 3 prints object in both, because globalThis is the global object in every modern runtime.

Two windows show what typeof gives for three names. In a browser page: window is object, process is undefined, globalThis is object. In Node.js: window is undefined, process is object, globalThis is object.browser pagewindowobjectprocessundefinedglobalThisobjectNode.jswindowundefinedprocessobjectglobalThisobject
Each row shows what typeof gives for that name. A browser page has window, Node.js has process, and both have globalThis.

This check is a clue, not proof. For example, a web worker is a script that runs in the background of a browser page. It is in a browser, but it has no window either.

Keep one rule in mind: code written only for Node.js does not run in a browser. So in this lesson, you can't run the Node.js, Deno, Bun, and terminal examples on the page. Instead, the lesson's tests run the Node.js examples in real Node.js to check what they print.

What Node.js adds to JavaScript

NODE HOST

Node.js is not a new language. It runs normal JavaScript, outside a browser. It is built from these parts:

  • V8 is the engine. It runs the language. Chrome uses the same engine.
  • libuv is a helper library. It lets Node.js wait for files and network data without stopping other work.
  • Built-in modules are ready-made tools for files, paths, and the computer. A module is a file of code you can import.
  • npm is a huge online collection of packages. A package is code that someone wrote and shared. The npm & package.json lesson covers it.

Plain Node.js has no window, no document, and no page elements. Use globalThis when you need the global object. That name works in browsers and in Node.js. Use process for facts about the running Node.js program.

A command-line tool (CLI) is a program you start by typing a command in a terminal. In a Node.js CLI, process.argv is an array of the words in the command that started the program. process.env holds environment variables: named settings from the terminal, always stored as strings.

To try the code below, run it with node --input-type=module -e and put tea as the last word of the command. It can't run on this page.

Read a command word and a setting in Node.jsJavaScript
process.env.RUNTIME_LESSON_NAME = "tea";
console.log(process.argv.slice(1).join(","));
console.log(typeof process.env.RUNTIME_LESSON_NAME);

Line 1 sets a sample environment variable. Line 2 prints tea, the last word of the command. Line 3 prints string, because environment variables are always strings. So check and convert a value before you use it as a number or as true or false. For example, the string "false" is truthy: an if treats it as true.

Node.js has many built-in modules. The table lists the ones that tools and servers use most. Each row says what one module lets you do.

Built-in Node.js modules you will see often.
ModuleWhat does it let you do?
fs / fs/promisesRead and write files. The fs/promises version returns promises, so you can use await.
pathBuild file paths with the right separator for each operating system.
osRead facts about the computer, such as its system type and home folder.
processRead the command words (argv), the environment variables (env), and the current folder, and set the exitCode.
child_processStart another program and read its exit code.
httpMake a web server or send web requests. Many server frameworks are built on it.
streamHandle big data in small chunks, so it never all sits in memory at once.
cryptoMake hashes, sign and encrypt data, and make random IDs. Node.js also has Web Crypto.

The script below uses several of these modules. It reads a word from the command, writes a file, reads it back, and builds a file path. It can't run on this page, because the page runs in a browser, not in Node.js.

A Node.js script that uses files, paths, and the computerJavaScript
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 readFile and writeFile. They read and write files, and they return promises.
  • Line 2 imports path. Line 7 uses path.join to build a file path. It puts the right separator between the parts: / on Mac and Linux, \ on Windows.
  • Line 6 reads the first word typed after the code, from process.argv.
  • Lines 9 and 10 write the message to a file and read it back.
  • Line 11 sets process.exitCode. An exit code is a number a program reports when it ends: 0 means success, and any other number means failure. Setting it does not stop the script.

Node.js has two ways to split code into modules. The code below shows both, doing the same job.

The same job in an ES module and a CommonJS moduleJavaScript
// 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"));};
  • The top half is an ES module (ESM), the standard JavaScript module system. Line 2 uses import, and line 3 uses export.
  • The bottom half is CommonJS, the older Node.js module system. Line 8 uses require, and line 9 uses module.exports.

Node.js supports both. The CommonJS & ESM lesson explains how Node.js picks between them.

Command words, files, other programs, and exit codes

STEP THROUGH

Many Node.js programs are command-line tools. Node.js gives such a tool four things:

  • the words typed in the command, in process.argv
  • the environment variables, in process.env
  • the folder the command was typed in, from process.cwd()
  • a way to report success or failure, with process.exitCode

The parser below turns command words into an object. It uses no Node.js tools, so you can step through it here in the browser. In a real Node.js tool, you would pass it process.argv.slice(2): the words after node and the script name.

Three command words turn into three parts of the options object. build goes into the underscore list. --watch becomes watch with the string true. --out=dist becomes out with the string dist.command wordstored in optionsbuild_: ["build"]--watchwatch: "true"--out=distout: "dist"
A word without -- goes into the _ list. A --name=value word becomes a property, and a flag with no = gets the string "true".
Step through the command-word parser
Step 0 of 9Ready
Your turn: follow the blue line

Step through a parser that turns command words into an object. It runs in the browser. In Node.js, only the words would come from process.argv.

Running in
  1. script
Next: line 1
  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));
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
Choose the command words to pass in

Choosing other words starts the steps again. In real Node.js, these words would come from process.argv.slice(2).

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 parser with other command words
The command-word parserPop 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
command words["build","--watch","--out=dist"]
printed JSON{"_":["build"],"watch":"true","out":"dist"}
Try it yourself

Pick a command. You can test this function in a browser, then use it with process.argv.slice(2) in a real Node.js tool.

The output comes from the same parseCliArgs function as the step-through above.

Reading a whole file at once is fine for a small settings file. For big files, such as logs or uploads, use a stream. A stream hands you the data in small pieces, called chunks, one after another. Your program never has to hold the whole file in memory.

The Streams & progress lesson covers browser streams in depth. Node.js has two kinds of stream: its own older streams, and the web streams that browsers use.

Read a file in small chunks with a Node.js streamJavaScript
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 6 writes a small log file. It has 21 characters.
  • Line 10 opens a stream that hands out 8 bytes at a time. The for await loop waits for each chunk, then runs its body once per chunk.
  • Lines 11 and 12 count the chunks and the characters.
  • Lines 15 and 16 print 3 chunks and 21 characters.

In a real program, the loop body might read one row of a spreadsheet file, or send each chunk on to a web response.

Web streams are the streams that browsers use. They are different from the older Node.js streams. A Response is an object that holds a web response: a status number, headers, and a body. A Response can read a web ReadableStream directly. This example works in browsers and in modern Node.js.

Read a tiny web stream as textPop out in the code editor (opens in a new tab)JavaScript
const stream = new ReadableStream({
  start(controller) {
    controller.enqueue(new TextEncoder().encode("tea"));
    controller.close();
  },
});
new Response(stream).text().then((text) => console.log(text));

Line 3 turns tea into bytes with TextEncoder, because a Response body is made of bytes, not strings. Line 4 closes the stream: no more chunks are coming. Line 7 reads the whole body as text and prints tea.

Reading everything with text() is fine for a tiny body. For a large body, read it chunk by chunk instead.

The next script does three more Node.js jobs. It makes a hash, starts a second program, and runs a tiny web server.

Node.js hashing, a child process, and a web serverJavaScript
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 5 uses the crypto module to make a hash: a short fingerprint of some text. Line 23 prints its first 8 characters, 8166e7dd.
  • Line 6 starts a child process: a second program, started by this one. The child sets its exit code to 7, so line 24 prints 7.
  • Lines 7 to 21 start a small web server, send it one request for /health, and close it. Line 25 prints the server's reply, ok /health.

Line 5 uses the older Node.js crypto module. The section on shared web APIs shows Web Crypto, the version that browsers also have.

Deno & Bun: two more runtimes outside the browser

TOOLS

Deno and Bun are two more runtimes for servers and tools. This lesson can't run them, so it shows no made-up output and no speed numbers. The commands below show what you would type. They follow the official Deno and Bun documentation.

Deno: safe by default and close to the web

Deno has these main features:

  • It uses V8, the same engine as Node.js and Chrome.
  • It is secure by default. A script can't read files, use the network, read environment variables, or start other programs until you allow it.
  • It runs TypeScript files directly, with no extra setup. TypeScript is JavaScript with types added.
  • It has the same web APIs that browsers have, such as fetch.
  • It can import npm packages with an npm: prefix.
  • It keeps project settings and tasks in a deno.json or deno.jsonc file.

You allow access with a permission flag on the command, such as --allow-read for files or --allow-net for the network. If a flag is missing and you are at a terminal, Deno may ask you. If no one can answer, as on a server, the action fails instead.

Deno commands with and without permissionsBash
deno run main.tsdeno run --allow-read=./data main.tsdeno run --allow-net=api.example.com server.tsdeno run -A trusted-tool.ts
  • Line 1 has no flags, so the script gets no access to files or the network.
  • Line 2 lets the script read only the ./data folder.
  • Line 3 lets the script talk only to api.example.com.
  • Line 4 uses -A, which allows everything, even starting other programs. Use -A only for code you trust.
Left: deno run main.ts with no flag. main.ts tries to read the ./data folder and has no access yet. Right: with the flag --allow-read=./data, main.ts can read ./data.deno run main.tsno flagmain.ts./datano access yet--allow-read=./dataflag addedmain.ts./datacan read ./data
With no flag, main.ts has no access to ./data yet. The flag --allow-read=./data lets it read that one folder.

Give a tool only the folder or website it needs. A script can also check a permission without asking for it:

Check a Deno file permissionJavaScript
const read = await Deno.permissions.query({ name: "read", path: "./data" });
if (read.state !== "granted") {
  console.log("No access to ./data yet");
}

Line 1 asks whether the script may read ./data. The answer has a state: granted, prompt, or denied. Line 2 checks for anything other than granted, so line 3 prints a message only when access is missing.

This code runs in Deno, not on this page. Checking a permission does not grant it. Also, every module your script imports gets the same permissions as the script.

A deno.json file keeps a project's settings in one place:

A deno.json file with a task, imports, and TypeScript settingsJSON
{  "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  }}
  • Lines 2 to 4 make a task named start, with its permission flags.
  • Line 6 links the name @std/path to a package on JSR, a package site made for Deno.
  • Line 7 links the name chalk to a package on npm.
  • Lines 9 and 10 turn on strict TypeScript checks.

Bun: one tool that does many jobs

Bun uses JavaScriptCore, the engine from Safari, instead of V8. Bun is four tools in one:

  • a runtime that runs your code
  • a package manager that installs packages
  • a bundler that joins many code files into a few files for the browser
  • a test runner that runs your tests

Bun also works to run code written for Node.js. Choose Bun because of these facts. Don't choose it from speed numbers copied from the internet, because those change with each version and each task.

Bun commands to run, test, bundle, and installBash
bun run index.tsbun testbun build ./src/app.ts --outdir=distbun add zod

Line 1 runs a file. Line 2 runs the tests. Line 3 bundles src/app.ts into the dist folder. Line 4 installs the zod package.

Bun also has its own global object, Bun. Use it only in code meant for Bun. Code that should also run in browsers or Node.js should use an API they all share, or keep the Bun part in a separate file.

Read a cart file in BunJavaScript
const file = Bun.file("./cart.json");
if (await file.exists()) {
  const cart = await file.json();
  console.log(cart.items.length);
}

Line 1 points at a local file, cart.json. Line 2 checks that the file exists. Line 3 reads the file and turns its JSON into an object. Line 4 prints how many items the cart has. This code runs only in Bun, and what it prints depends on the file.

Deno and Bun in one sentence each

Deno puts safety first: scripts need permission flags, it follows web standards, and it runs TypeScript directly. Bun puts many jobs in one tool: it runs code, installs packages, bundles, and tests, on the JavaScriptCore engine, and it aims to run Node.js code too.

Edge runtimes: small request handlers near users

REQUESTS

An edge runtime runs your code on many servers around the world, close to the people who use a site. Examples are Cloudflare Workers, Vercel Edge Functions, Netlify Edge Functions, and Deno Deploy.

Edge code is different from a normal Node.js program:

  • It usually can't read or write files on the server.
  • It is a small request handler: a function that gets a Request and returns a Response.
  • It usually runs in an isolate: a small, sealed-off space inside the engine, kept apart from other people's code.
A Request card for the address ending in ?name=Ada goes into the handleRequest function, which reads the URL. A Response card with status 200 comes out.RequesthandleRequestResponse?name=Adareads the URLstatus 200
The handler gets a Request and gives back a Response. For /api/greet?name=Ada, the response has status 200.
Step through a request handler
Step 0 of 10Ready
Your turn: follow the blue line

Step through a handler that turns a Request into a Response. The page, a Node.js test, or an edge platform can all call the same handler.

Running in
  1. script
Next: line 1
  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"));
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
Choose the request to pass in

An edge platform calls a handler with a real Request. Here, the page calls the same function.

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
A request 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 addresshttps://lesson.test/api/greet?name=Ada
statusnot called yet
x-runtimenot called yet
bodynot called yet
Try it yourself

Many edge runtimes call a function like this one: it gets a Request and returns a Response. It needs no page, no window, and no local fs.

Choose a request, then press Call handler. The page makes a real Request, gets back a real Response, and reads its body.

Not every response has a body. When a request works but there is nothing to send back, return status 204, which means "no content". Give it a body of null. You can test Request and Response objects in a browser or in Node.js, without putting any code on an edge platform.

A cart response with no contentPop out in the code editor (opens in a new tab)JavaScript
const request = new Request("https://shop.test/cart");
const response = new Response(null, { status: 204, headers: { "x-cart-state": "empty" } });
console.log(new URL(request.url).pathname);
console.log(response.status);
console.log(response.headers.get("x-cart-state"));
console.log(response.body === null);

Line 1 makes a request for /cart. Line 2 makes a response with status 204, no body, and one custom header. Lines 3 to 6 print /cart, 204, empty, and true.

A 204 response must have no body. Passing a body with status 204 throws a TypeError. Each edge platform also has its own way to connect your handler to incoming requests.

Edge platforms set strict limits on purpose. Expect these:

  • a limit on CPU time and memory for each request
  • no local files
  • no child processes
  • only some of the Node.js built-in modules, or none

The exact limits depend on the platform and the price plan. So keep the work for each request short, send data as a stream when you can, and save data in the platform's storage, not on a local disk.

An edge platform's 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 code is in the style of Cloudflare Workers. For each request, the platform calls the fetch method on line 2.
  • Line 3 passes the request to handleRequest, which returns a Response.
  • Line 8 reads a header that the platform adds: the visitor's country.
  • The env argument holds the platform's bindings. A binding is a link to the platform's storage or settings, handed to your code as an object.

handleRequest uses only shared web APIs. So you can reuse it on other runtimes, and test it without the platform.

Web APIs that most runtimes share

PORTABLE

Modern runtimes share more APIs than they used to. These work in browsers, Node.js, Deno, Bun, and most edge runtimes:

  • requests and responses: fetch, Request, Response, Headers
  • web addresses: URL, URLSearchParams
  • data: web streams, TextEncoder, structuredClone
  • security: Web Crypto, such as crypto.subtle and crypto.randomUUID
  • timing and events: AbortController, setTimeout, EventTarget

A standards group works on this shared list. It is called WinterTC, and its official name is Ecma Technical Committee 55 (TC55). Ecma is the same group that publishes the JavaScript standard. WinterTC grew out of an earlier community group called WinterCG.

In December 2025, WinterTC published ECMA-429, the Minimum Common Web API. It lists the web APIs that server runtimes should all have. It is a goal for runtimes, not a promise that every runtime has every API in the list above. So check the runtime and version you will use for the exact API you need.

When your code needs one feature, use feature detection: check that the feature exists. Don't guess from the runtime's name. The code below checks that fetch is a function. It also shows that the page (document) and the running program (process) are two separate tools.

Check for the features you needPop out in the code editor (opens in a new tab)JavaScript
console.log(typeof fetch === "function");
console.log(typeof document === "object");
console.log(typeof process === "object");

In a browser page, lines 1 to 3 print true, true, false. In Node.js 22, they print true, false, true. Some cases are less clear:

  • A web worker has fetch but no document.
  • A platform can have a fetch that follows different network rules.
  • In Deno, finding fetch does not give you permission to use the network.
Try the shared web APIs on this page
Shared web APIs in one scriptPop 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");
Outputthis page
  1. edge:4:fetch
  2. true
  3. function
  4. true
  5. false
  6. true
Try it yourself
Real values from this page

This code uses only APIs that modern browsers, Node.js 22, Deno, Bun, and most edge runtimes all have. The lesson's tests run it in Node.js and in Chrome.

crypto.randomUUID() makes a random ID each time. So the code prints whether the ID is 36 characters long, not the ID itself.

An AbortController lets your code say that some work is no longer needed. You pass its signal to an API that can stop early, such as fetch. Aborting stops the work on your side. It does not promise that the server stopped too.

Cancel work with a signalPop out in the code editor (opens in a new tab)JavaScript
const controller = new AbortController();
controller.abort();
console.log(controller.signal.aborted);

Line 1 makes a controller. Line 2 aborts it. Line 3 prints true, because the signal is now aborted. You don't need a network request to see this.

Which runtimes have 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

Put each API in its group: a shared web API, a Node.js built-in, a browser page API, or a feature of one edge platform.

Choose a category for every card. You can change an answer at any time; Reset clears them all.
Shared APIs can still behave differently

Even when the names match, runtimes can differ in network permissions, cookies, security certificates, stream helpers, and limits. So test your code in the runtime you will use. And keep Node.js-only imports out of code meant for browsers or edge handlers.

Where you’ll use runtime knowledge

Real projects often use more than one runtime. Build tools and CLIs run in Node.js, Deno, or Bun. Web apps run in browsers. Server code may run in Node.js or on an edge platform. Some packages even ship one version for browsers and another for Node.js.

  • CLIs: read process.argv, check the flags, set process.exitCode, and stream the output.
  • Build tools: read project files, start compilers as child processes, hash files, and write bundles.
  • Servers: use the Node.js http module, or a framework built on it, for code that keeps running.
  • Edge pages: read a Request, read a platform binding, and quickly return a small Response made for that visitor.
  • Shared helpers: use URL, Request, web streams, and Web Crypto when the code should run anywhere.
A quick runtime checklist

Before you add a package or call an API, ask what the code needs. Does it need the page? A Node.js built-in module? A Deno permission flag? An edge binding? Or only the shared web APIs?

Common misconceptions

  • “JavaScript outside the browser can use the DOM.” The DOM is the page, and only a browser page has one. A server can read HTML with a library, but it has no real page by default.
  • “Node.js built-in modules are part of JavaScript.” Names like fs, path, and child_process are tools that Node.js adds. The language itself doesn't have them.
  • “Deno and Bun are just Node.js with new commands.” They make different choices: Deno asks for permissions, and Bun uses a different engine and bundles many tools.
  • “An edge runtime is a full server with no limits.” Edge code runs close to users, but it has strict limits on CPU time, memory, and which APIs it can use.
  • “A shared web API works exactly the same everywhere.” The names and shapes match, but each runtime still has its own rules and limits.

The table asks four questions about each runtime. Read down a column to see one runtime, or across a row to compare them.

The same questions, answered for each runtime.
QuestionBrowserNode.jsDenoBunEdge
Can my code read local files?No, a normal page can't.Yes, with fs.Yes, but only with a permission flag.Yes, for servers and tools.Usually no, there is no local fs.
Do I have window and document?Yes.No.No.No, there is no real page by default.No, there is no real page.
How does the code usually start?An event listener runs when the user does something.A command starts a script, or a server calls a function for each request.A command starts a script, a task, or a server.A command starts a script, tests, a build, or a server.The platform calls fetch(request), which returns a Response.
What should code that runs anywhere use?Web APIs.Shared web APIs, plus small Node.js-only files where needed.Web APIs, with the right permission flags.Web APIs, plus its support for Node.js code.The Minimum Common Web API.

Practice exercises

5 EXERCISES
Exercise 1 · Warm-upPredict what the parser prints

Read the parser. 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 · PracticeWhich group does Request belong to?

    Your code should run in browsers, Node.js 22, Deno, Bun, and edge runtimes. Which group of APIs does Request belong to?

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

      Exercise 3 · PracticePredict the handler's status

      Predict the status number that the request handler prints.

      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 a failure and still let cleanup finish

        A Node.js CLI found a bad input and printed an error message. It still has some cleanup work waiting to run. Which property should it set to report the failure?

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

          Exercise 5 · ChallengeChoose a runtime for a page made for each visitor

          A site wants to read each visitor's request headers and send back a small response made for that visitor. It should run close to users. It does not need local files. Which kind of runtime fits best?

          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 is Node.js built from?

              Choose an answer to see the explanation.

            2. Question 2 of 8What does this command-word 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 8Why can't this lesson's Node.js-only examples run on the page?

              Choose an answer to see the explanation.

            4. Question 4 of 8How do you tell an ES module from a CommonJS module in Node.js code?

              Choose an answer to see the explanation.

            5. Question 5 of 8Which Deno command lets a script read files?

              Choose an answer to see the explanation.

            6. Question 6 of 8Why does Node.js code that reads files usually fail on an edge runtime?

              Choose an answer to see the explanation.

            7. Question 7 of 8Which group is made of shared web APIs that most runtimes have?

              Choose an answer to see the explanation.

            8. Question 8 of 8Which sentence about Bun is true, without counting on speed numbers?

              Choose an answer to see the explanation.

            Key takeaways

            • Node.js is the V8 engine plus libuv, built-in modules, process, and npm packages. It is not a browser page.
            • Use process.argv, process.env, and process.exitCode in command-line tools. Use streams for big files.
            • Deno asks for permissions, runs TypeScript directly, has web APIs, imports npm packages with npm:, and keeps settings in deno.json.
            • Bun uses the JavaScriptCore engine. It is a runtime, package manager, bundler, and test runner in one, and it aims to run Node.js code.
            • Edge runtimes run small handlers that turn a Request into a Response, with web APIs, platform bindings, and strict limits.
            • For code that should run in browsers, Node.js, Deno, Bun, and edge runtimes, start with the shared web APIs.

            Remember the one-liner.
            The JavaScript language runs almost anywhere; the runtime decides which tools your code can use.

            Up next: Unit testing.

            CompleteFrontend Clear concepts. Working examples.