JavaScript: Browser vs Server — What's Actually Different?

JavaScript is the same language whether it runs in Chrome or on a server. Same const, same for loops, same Promise. So why does server-side JavaScript feel so different?
Because the language is only half the story. The other half is the environment — and the browser and server environments are nothing alike.
This post breaks down exactly what changes when JavaScript leaves the browser and runs on a server with Node.js, Bun, or Deno.
What You'll Learn
Learning Outcomes:
✅ Why the same language behaves differently in two environments
✅ What APIs the browser gives you vs what the server gives you
✅ How global objects, modules, and async patterns differ
✅ Key differences between Node.js, Bun, and Deno as server runtimes
✅ Common pitfalls when switching between browser and server code
✅ How to write code that works in both environments
The Core Idea: Same Engine, Different Dashboard
Think of JavaScript like a car engine. The engine (V8, SpiderMonkey, JavaScriptCore) runs the same JavaScript code. But the dashboard — the controls, buttons, and gauges available to you — depends on where the engine is installed.
The JavaScript spec (ECMAScript) defines the language: variables, functions, classes, promises, iterators. Everything else — DOM manipulation, file I/O, HTTP servers — comes from the host environment.
Part 1: What the Browser Gives You
When JavaScript runs in a browser (Chrome, Firefox, Safari), you get access to Web APIs provided by the browser vendor. These APIs are standardized by the W3C and WHATWG.
The window Global Object
In the browser, the top-level global object is window. Everything hangs off it:
// Browser globals
console.log(window.location.href); // Current URL
console.log(window.innerWidth); // Viewport width
console.log(window.navigator.userAgent); // Browser info
// "window" is implicit
alert("Hello!"); // Same as window.alert("Hello!")
setTimeout(() => {}, 1000); // Same as window.setTimeout(...)DOM — The Browser's Killer Feature
The Document Object Model is the API that lets JavaScript interact with HTML. It does not exist on the server.
// DOM manipulation — browser ONLY
const button = document.getElementById("submit-btn");
button.addEventListener("click", () => {
const input = document.querySelector(".email-input");
const div = document.createElement("div");
div.textContent = `Submitted: ${input.value}`;
document.body.appendChild(div);
});Why it's browser-only: The DOM is a live representation of the HTML document rendered on screen. A server has no screen, no HTML page, no user clicking buttons.
Browser-Only APIs
Here are APIs you can use in the browser but not on a server (unless polyfilled):
| API | What It Does |
|---|---|
document / DOM | Manipulate HTML elements |
window.location | Current URL and navigation |
window.history | Browser history (back/forward) |
localStorage / sessionStorage | Key-value storage in the browser |
navigator.geolocation | User's GPS location |
Canvas / WebGL | 2D/3D graphics rendering |
Web Audio API | Audio processing |
IntersectionObserver | Track element visibility |
Service Workers | Offline caching and background sync |
WebRTC | Peer-to-peer video/audio |
How Browser JavaScript Loads
Browser JS is loaded via <script> tags and executes in a sandboxed environment:
<!-- Traditional script loading -->
<script src="app.js"></script>
<!-- ES Module in the browser -->
<script type="module" src="app.mjs"></script>
<!-- Inline script -->
<script>
console.log("I run in the browser!");
</script>Key constraint: Browser JavaScript is sandboxed. It cannot:
- Read or write files on the user's computer
- Access other browser tabs (same-origin policy)
- Make requests to different domains without CORS
- Run system commands
This sandboxing exists for security — you don't want a random website reading your files.
Part 2: What the Server Gives You
When JavaScript runs on a server runtime (Node.js, Bun, or Deno), the sandbox is gone. You get direct access to the operating system.
The Global Object — It's Not window
Each runtime has its own global:
// Node.js
console.log(globalThis); // The universal way (works everywhere)
console.log(global); // Node.js-specific (legacy)
// Deno
console.log(globalThis); // Uses standard globalThis
// Deno.version, Deno.env, etc.
// Bun
console.log(globalThis); // Uses standard globalThis
// Bun.version, Bun.env, etc.Tip: Use
globalThis— it works in browsers, Node.js, Deno, and Bun. It's the standard way to reference the global object.
File System Access
The biggest difference: servers can read and write files.
// Node.js
import { readFile, writeFile } from "node:fs/promises";
const data = await readFile("config.json", "utf-8");
const config = JSON.parse(data);
config.updatedAt = new Date().toISOString();
await writeFile("config.json", JSON.stringify(config, null, 2));
// Deno
const data = await Deno.readTextFile("config.json");
await Deno.writeTextFile("config.json", newContent);
// Bun
const file = Bun.file("config.json");
const data = await file.text();
await Bun.write("config.json", newContent);Creating HTTP Servers
In the browser, you send HTTP requests. On the server, you receive them.
// Node.js (built-in)
import { createServer } from "node:http";
const server = createServer((req, res) => {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ message: "Hello from Node.js!" }));
});
server.listen(3000);
// Deno
Deno.serve({ port: 3000 }, (req) => {
return new Response(JSON.stringify({ message: "Hello from Deno!" }), {
headers: { "Content-Type": "application/json" },
});
});
// Bun
Bun.serve({
port: 3000,
fetch(req) {
return new Response(JSON.stringify({ message: "Hello from Bun!" }), {
headers: { "Content-Type": "application/json" },
});
},
});Server-Only APIs
| API | What It Does |
|---|---|
fs (File System) | Read, write, delete files and directories |
path | Manipulate file paths cross-platform |
http / https | Create HTTP servers and clients |
net | Low-level TCP/UDP networking |
child_process | Spawn other programs and scripts |
os | CPU, memory, hostname info |
crypto | Hashing, encryption, certificates |
stream | Process large data in chunks |
process / Deno.env / Bun.env | Environment variables and process info |
worker_threads | Multi-threaded parallelism |
Part 3: Side-by-Side Comparison
Let's put the two environments next to each other:
| Aspect | Browser | Server (Node.js / Bun / Deno) |
|---|---|---|
| Global object | window | globalThis / global / Deno |
| DOM access | Yes | No (unless using JSDOM) |
| File system | No | Yes (fs, Deno.readFile, Bun.file) |
| HTTP role | Client (sends requests) | Server (receives requests) |
| Module system | ES Modules via <script type="module"> | ES Modules + CommonJS (Node.js) |
| Package manager | None (uses CDN or bundler) | npm, yarn, pnpm, bun |
| Security | Sandboxed (can't access OS) | Full OS access (or permission-based in Deno) |
| User interaction | Click, keyboard, touch events | stdin, CLI arguments, HTTP requests |
| Concurrency | Web Workers | Worker Threads, Cluster, Child Processes |
| Entry point | <script> tag in HTML | node app.js / bun app.ts / deno run app.ts |
Part 4: Module Systems — The Confusing Part
One of the biggest sources of confusion when switching between browser and server JavaScript is modules.
Browser: ES Modules Only
Modern browsers support ES Modules natively:
<script type="module">
import { formatDate } from "./utils.js";
console.log(formatDate(new Date()));
</script>Rules in the browser:
- Must use file extensions (
.js,.mjs) - Imports are relative URLs (
./utils.js, notutils) - No bare imports like
import express from "express"(unless using import maps) - Loaded asynchronously, deferred by default
Server: It's More Complicated
Node.js grew up with CommonJS and later added ES Modules. Bun and Deno use ES Modules by default.
// CommonJS (Node.js legacy — still widely used)
const express = require("express");
const path = require("path");
module.exports = { myFunction };
// ES Modules (Modern — works in Node, Bun, Deno)
import express from "express";
import { join } from "node:path";
export function myFunction() {}How each runtime handles modules:
| Feature | Node.js | Bun | Deno |
|---|---|---|---|
| Default | CommonJS | ES Modules | ES Modules |
| ES Modules | .mjs or "type": "module" in package.json | Always | Always |
| CommonJS | .cjs or default | Supported | Limited support |
| Import from URL | No | No | Yes (import x from "https://...") |
| Bare imports | Yes (import x from "pkg") | Yes | Yes (with import map or npm: prefix) |
// Deno — can import from URLs
import { serve } from "https://deno.land/std/http/server.ts";
// Deno — npm packages with prefix
import express from "npm:express";
// Node.js — built-in modules now use node: prefix
import { readFile } from "node:fs/promises";Part 5: APIs That Exist in Both
Over the years, browsers and server runtimes have converged on several APIs. These now work in both environments:
Fetch API
Originally browser-only, now available everywhere:
// Works in browser, Node.js 18+, Bun, and Deno
const response = await fetch("https://api.example.com/users");
const users = await response.json();
console.log(users);Web Crypto API
// Works everywhere
const hash = await crypto.subtle.digest(
"SHA-256",
new TextEncoder().encode("hello")
);Other Shared APIs
| API | Status |
|---|---|
fetch() | Available in all modern environments |
URL / URLSearchParams | Available everywhere |
TextEncoder / TextDecoder | Available everywhere |
crypto.subtle | Available everywhere |
AbortController | Available everywhere |
structuredClone() | Available everywhere |
setTimeout / setInterval | Available everywhere |
console.* | Available everywhere |
WebSocket | Available everywhere |
ReadableStream / WritableStream | Available everywhere |
Blob / File | Available everywhere (Node.js 18+) |
FormData | Available everywhere (Node.js 18+) |
This convergence is great news — it means more of your code is portable between environments.
Part 6: The Three Server Runtimes Compared
Now let's briefly compare what makes Node.js, Bun, and Deno different as server environments.
For a deep dive, see Bun vs Node.js vs Deno: Complete Comparison Guide.
Node.js — The Established Standard
// Node.js style
import express from "express";
import { readFile } from "node:fs/promises";
const app = express();
app.get("/config", async (req, res) => {
const data = await readFile("./config.json", "utf-8");
res.json(JSON.parse(data));
});
app.listen(3000);Strengths: Massive ecosystem, battle-tested, universal support, most hiring demand.
Deno — The Secure Alternative
// Deno style — TypeScript out of the box
import { Application, Router } from "https://deno.land/x/oak/mod.ts";
const router = new Router();
router.get("/config", async (ctx) => {
const data = await Deno.readTextFile("./config.json");
ctx.response.body = JSON.parse(data);
});
const app = new Application();
app.use(router.routes());
await app.listen({ port: 3000 });Strengths: Built-in TypeScript, permission-based security, web-standard APIs, URL imports.
Bun — The Fast One
// Bun style — minimal boilerplate
const server = Bun.serve({
port: 3000,
async fetch(req) {
const url = new URL(req.url);
if (url.pathname === "/config") {
const file = Bun.file("./config.json");
return new Response(file, {
headers: { "Content-Type": "application/json" },
});
}
return new Response("Not Found", { status: 404 });
},
});
console.log(`Listening on ${server.url}`);Strengths: Fastest startup and runtime performance, built-in bundler/test runner, drop-in Node.js replacement.
Quick Runtime Comparison
| Aspect | Node.js | Deno | Bun |
|---|---|---|---|
| TypeScript | Requires tsx or ts-node | Built-in | Built-in |
| Security | Full OS access by default | Permission-based (--allow-read, etc.) | Full OS access by default |
| Performance | Good | Good | Fastest |
| npm compatibility | Native | High (via npm: prefix) | High |
| Stability | Most mature | Stable | Stable |
| Best for | Enterprise, existing projects | Security-focused apps | Performance-critical apps |
Part 7: Common Gotchas When Switching
1. window is Not Defined
The #1 error when running browser code on the server:
// ❌ This crashes on the server
if (window.innerWidth > 768) {
showMobileMenu();
}
// ✅ Check first
if (typeof window !== "undefined") {
// We're in the browser
if (window.innerWidth > 768) {
showMobileMenu();
}
}2. document is Not Defined
Same problem — DOM doesn't exist on the server:
// ❌ Crashes on server
const el = document.getElementById("app");
// ✅ Guard it
if (typeof document !== "undefined") {
const el = document.getElementById("app");
}3. __dirname and __filename in ES Modules
CommonJS provides __dirname and __filename, but ES Modules don't:
// CommonJS — works
console.log(__dirname); // /home/user/project
console.log(__filename); // /home/user/project/app.js
// ES Modules — need to derive it
import { fileURLToPath } from "node:url";
import { dirname } from "node:path";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Bun and Deno shortcuts
console.log(import.meta.dir); // Bun
console.log(import.meta.dirname); // Node.js 21+4. Fetch Needs No Import (Now)
Before Node.js 18, you needed node-fetch. Now fetch is built-in everywhere:
// ❌ Old way (Node.js < 18)
import fetch from "node-fetch";
// ✅ Modern — just use it
const res = await fetch("https://api.example.com/data");5. require() vs import
Mixing module systems causes errors:
// ❌ Can't use require() in ES Module
import express from "express";
const config = require("./config"); // SyntaxError!
// ✅ Use import consistently
import express from "express";
import config from "./config.js";
// ✅ Or use dynamic import
const config = await import("./config.js");Part 8: Writing Universal (Isomorphic) Code
Sometimes you need code that works in both browser and server. Here are practical patterns:
Pattern 1: Environment Detection
const isBrowser = typeof window !== "undefined";
const isServer = typeof window === "undefined";
const isNode = typeof process !== "undefined" && process.versions?.node;
const isDeno = typeof Deno !== "undefined";
const isBun = typeof Bun !== "undefined";
if (isBrowser) {
// Browser-specific code
localStorage.setItem("theme", "dark");
} else {
// Server-specific code
console.log("Running on server");
}Pattern 2: Conditional Exports in package.json
{
"name": "my-library",
"exports": {
".": {
"browser": "./dist/browser.js",
"node": "./dist/server.js",
"default": "./dist/server.js"
}
}
}Pattern 3: Abstract the Difference
// storage.js — works everywhere
export function getStorage() {
if (typeof window !== "undefined") {
// Browser: use localStorage
return {
get: (key) => localStorage.getItem(key),
set: (key, value) => localStorage.setItem(key, value),
};
} else {
// Server: use in-memory store (or file/database)
const store = new Map();
return {
get: (key) => store.get(key),
set: (key, value) => store.set(key, value),
};
}
}Part 9: Visual Summary
Part 10: When Should You Care?
Here's a practical decision guide:
You're writing browser-only code if:
- You manipulate HTML/CSS (DOM)
- You respond to user events (clicks, keyboard)
- You use
localStorage,sessionStorage, or cookies viadocument.cookie - You're building a frontend with React, Vue, Angular, etc.
You're writing server-only code if:
- You read/write files
- You connect to databases
- You create HTTP endpoints
- You access environment variables or system resources
- You run background jobs or cron tasks
You're writing universal code if:
- You build a library used in both environments
- You work with frameworks like Next.js (SSR + client)
- You write utility functions (date formatting, validation, string manipulation)
Key Takeaways
-
Same language, different environment. JavaScript syntax is identical everywhere. The APIs change based on where it runs.
-
Browser = sandboxed client. You get DOM, Web APIs, and network requests. No file system, no OS access.
-
Server = unrestricted runtime. You get file I/O, HTTP servers, OS access, and system-level operations.
-
APIs are converging.
fetch,crypto,URL,streams— many APIs now work in both environments. -
Module systems still differ. CommonJS is server-legacy, ES Modules are the future everywhere. Deno and Bun default to ESM.
-
Use
globalThisand environment checks for portable code. -
The three runtimes (Node.js, Deno, Bun) give you the same power with different tradeoffs in security, performance, and DX.
Further Reading
- Bun vs Node.js vs Deno: Complete Comparison Guide — deep dive into runtime differences
- Getting Started with Express.js — build your first server
- HTTP Protocol Complete Guide — understand the protocol behind web requests
- JavaScript Build Tools & Bundlers Explained — how code goes from source to browser
📬 Subscribe to Newsletter
Get the latest blog posts delivered to your inbox every week. No spam, unsubscribe anytime.
We respect your privacy. Unsubscribe at any time.
💬 Comments
Sign in to leave a comment
We'll never post without your permission.