Back to blog

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

javascriptnodejsbundenoweb-development
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):

APIWhat It Does
document / DOMManipulate HTML elements
window.locationCurrent URL and navigation
window.historyBrowser history (back/forward)
localStorage / sessionStorageKey-value storage in the browser
navigator.geolocationUser's GPS location
Canvas / WebGL2D/3D graphics rendering
Web Audio APIAudio processing
IntersectionObserverTrack element visibility
Service WorkersOffline caching and background sync
WebRTCPeer-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

APIWhat It Does
fs (File System)Read, write, delete files and directories
pathManipulate file paths cross-platform
http / httpsCreate HTTP servers and clients
netLow-level TCP/UDP networking
child_processSpawn other programs and scripts
osCPU, memory, hostname info
cryptoHashing, encryption, certificates
streamProcess large data in chunks
process / Deno.env / Bun.envEnvironment variables and process info
worker_threadsMulti-threaded parallelism

Part 3: Side-by-Side Comparison

Let's put the two environments next to each other:

AspectBrowserServer (Node.js / Bun / Deno)
Global objectwindowglobalThis / global / Deno
DOM accessYesNo (unless using JSDOM)
File systemNoYes (fs, Deno.readFile, Bun.file)
HTTP roleClient (sends requests)Server (receives requests)
Module systemES Modules via <script type="module">ES Modules + CommonJS (Node.js)
Package managerNone (uses CDN or bundler)npm, yarn, pnpm, bun
SecuritySandboxed (can't access OS)Full OS access (or permission-based in Deno)
User interactionClick, keyboard, touch eventsstdin, CLI arguments, HTTP requests
ConcurrencyWeb WorkersWorker Threads, Cluster, Child Processes
Entry point<script> tag in HTMLnode 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, not utils)
  • 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:

FeatureNode.jsBunDeno
DefaultCommonJSES ModulesES Modules
ES Modules.mjs or "type": "module" in package.jsonAlwaysAlways
CommonJS.cjs or defaultSupportedLimited support
Import from URLNoNoYes (import x from "https://...")
Bare importsYes (import x from "pkg")YesYes (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

APIStatus
fetch()Available in all modern environments
URL / URLSearchParamsAvailable everywhere
TextEncoder / TextDecoderAvailable everywhere
crypto.subtleAvailable everywhere
AbortControllerAvailable everywhere
structuredClone()Available everywhere
setTimeout / setIntervalAvailable everywhere
console.*Available everywhere
WebSocketAvailable everywhere
ReadableStream / WritableStreamAvailable everywhere
Blob / FileAvailable everywhere (Node.js 18+)
FormDataAvailable 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

AspectNode.jsDenoBun
TypeScriptRequires tsx or ts-nodeBuilt-inBuilt-in
SecurityFull OS access by defaultPermission-based (--allow-read, etc.)Full OS access by default
PerformanceGoodGoodFastest
npm compatibilityNativeHigh (via npm: prefix)High
StabilityMost matureStableStable
Best forEnterprise, existing projectsSecurity-focused appsPerformance-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 via document.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

  1. Same language, different environment. JavaScript syntax is identical everywhere. The APIs change based on where it runs.

  2. Browser = sandboxed client. You get DOM, Web APIs, and network requests. No file system, no OS access.

  3. Server = unrestricted runtime. You get file I/O, HTTP servers, OS access, and system-level operations.

  4. APIs are converging. fetch, crypto, URL, streams — many APIs now work in both environments.

  5. Module systems still differ. CommonJS is server-legacy, ES Modules are the future everywhere. Deno and Bun default to ESM.

  6. Use globalThis and environment checks for portable code.

  7. The three runtimes (Node.js, Deno, Bun) give you the same power with different tradeoffs in security, performance, and DX.


Further Reading

📬 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.