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.
- 01Tell the language from the runtimeSay which extra tools Node.js, Deno, Bun, and edge runtimes add around the engine that runs JavaScript.
- 02Read Node.js codeSpot command words, files, paths, streams, other programs, exit codes, and the two module systems in Node.js code.
- 03Write 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.
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.
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.
| Runtime | What runs the code? | Which extra tools does it add? | What do people use it for? |
|---|---|---|---|
| Browser | An engine inside the browser: V8, SpiderMonkey, or JavaScriptCore. | It adds the page (document), window, storage, and user events. | Websites and web apps. |
| Node.js | The 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. |
| Deno | The 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. |
| Bun | The 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 runtime | Usually 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.
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.
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 HOSTNode.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.
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.
| Module | What does it let you do? |
|---|---|
fs / fs/promises | Read and write files. The fs/promises version returns promises, so you can use await. |
path | Build file paths with the right separator for each operating system. |
os | Read facts about the computer, such as its system type and home folder. |
process | Read the command words (argv), the environment variables (env), and the current folder, and set the exitCode. |
child_process | Start another program and read its exit code. |
http | Make a web server or send web requests. Many server frameworks are built on it. |
stream | Handle big data in small chunks, so it never all sits in memory at once. |
crypto | Make 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.
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
readFileandwriteFile. They read and write files, and they return promises. - Line 2 imports
path. Line 7 usespath.jointo 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.
// 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 usesexport. - The bottom half is CommonJS, the older Node.js module system. Line 8 uses
require, and line 9 usesmodule.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 THROUGHMany 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.
-- goes into the _ list. A --name=value word becomes a property, and a flag with no = gets the string "true".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.
script
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));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));["build","--watch","--out=dist"]{"_":["build"],"watch":"true","out":"dist"}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.
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.
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 awaitloop 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 chunksand21 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.
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.
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
cryptomodule 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
TOOLSDeno 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.jsonordeno.jsoncfile.
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 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
./datafolder. - Line 3 lets the script talk only to
api.example.com. - Line 4 uses
-A, which allows everything, even starting other programs. Use-Aonly for code you trust.
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:
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:
{ "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/pathto a package on JSR, a package site made for Deno. - Line 7 links the name
chalkto 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 run index.tsbun testbun build ./src/app.ts --outdir=distbun add zodLine 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.
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 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
REQUESTSAn 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
Requestand returns aResponse. - It usually runs in an isolate: a small, sealed-off space inside the engine, kept apart from other people's code.
Request and gives back a Response. For /api/greet?name=Ada, the response has status 200.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.
script
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"));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"));https://lesson.test/api/greet?name=Adanot called yetnot called yetnot called yetMany 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.
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.
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.
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
fetchmethod on line 2. - Line 3 passes the request to
handleRequest, which returns aResponse. - Line 8 reads a header that the platform adds: the visitor's country.
- The
envargument 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
PORTABLEModern 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.subtleandcrypto.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.
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
fetchbut nodocument. - A platform can have a
fetchthat follows different network rules. - In Deno, finding
fetchdoes not give you permission to use the network.
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");edge:4:fetchtruefunctiontruefalsetrue
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.
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.
fetch, Request, Response, HeadersURL and URLSearchParamsReadableStream and TextEncodercrypto.subtle and crypto.randomUUIDimport { readFile } from 'node:fs/promises'spawnSync(process.execPath, ['-e', code])document.querySelector('button')env.MY_KV.get('theme')
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.
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, setprocess.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
httpmodule, or a framework built on it, for code that keeps running. - Edge pages: read a
Request, read a platform binding, and quickly return a smallResponsemade for that visitor. - Shared helpers: use
URL,Request, web streams, and Web Crypto when the code should run anywhere.
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, andchild_processare 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.
| Question | Browser | Node.js | Deno | Bun | Edge |
|---|---|---|---|---|---|
| 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 EXERCISESRead the parser. Type the exact JSON string it prints.
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"])));The parser returns { _: ["serve"], port: "3000", watch: "true" }. Then JSON.stringify turns it into a string with no spaces: {"_":["serve"],"port":"3000","watch":"true"}.
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?
Request is a shared web API, together with fetch, Response, and Headers.
Predict the status number that the request handler prints.
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);The path /nope is not /api/greet, so the handler returns the last response, with status 404.
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?
Set process.exitCode = 1. Node.js uses that number when the program ends on its own, after all waiting work is done. Nothing is cut off.
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?
An edge runtime, such as Cloudflare Workers, Vercel Edge Functions, Netlify Edge Functions, or Deno Deploy. It runs a small request handler near users and needs no local files.
Check your understanding
8 QUESTIONSQuestion 1 of 8What is Node.js built from?
Choose an answer to see the explanation.
Question 2 of 8What does this command-word parser print?
Read the code, then predictfunction 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.
Question 3 of 8Why can't this lesson's Node.js-only examples run on the page?
Choose an answer to see the explanation.
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.
Question 5 of 8Which Deno command lets a script read files?
Choose an answer to see the explanation.
Question 6 of 8Why does Node.js code that reads files usually fail on an edge runtime?
Choose an answer to see the explanation.
Question 7 of 8Which group is made of shared web APIs that most runtimes have?
Choose an answer to see the explanation.
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, andprocess.exitCodein 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 indeno.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
Requestinto aResponse, 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.