Node.js — JavaScript on the server

Node.js lets JavaScript run outside the browser — on a server, on your laptop, anywhere. Understanding what it is and what it's not is the foundation of every backend project you'll ever build.

What is Node.js?

JavaScript was invented to run inside web browsers. For years, that was its only job — making web pages interactive. Node.js changed that.

Node.js is a runtime environment that lets JavaScript run directly on your computer (or a server), completely separate from a browser. It takes the V8 JavaScript engine (the same one that powers Google Chrome) and runs it as a standalone program.

JavaScript in a browser

  • Runs inside Chrome, Firefox, Safari
  • Can access the DOM (web page)
  • Can make fetch requests
  • Cannot read/write files on your computer
  • Cannot open network sockets
  • Sandboxed for security

JavaScript in Node.js

  • Runs directly on your computer or server
  • No browser, no DOM
  • Can read and write files (fs module)
  • Can open network ports and listen for requests
  • Can access environment variables
  • Can install and use npm packages

Why does Node.js exist?

Before Node.js, if you wanted to build a web server, you had to use a different language — PHP, Python, Ruby, Java. That meant frontend developers who knew JavaScript had to learn a completely different language and toolchain to build backends.

Node.js made it possible to write your entire application in one language — JavaScript — from the browser UI to the server handling requests to the scripts automating your build process. That dramatically reduced the context-switching needed to build web apps.

Today, Node.js is the backbone of the modern JavaScript ecosystem. Every tool you use as a frontend developer — Vite, webpack, ESLint, Prettier, TypeScript — runs on Node.js. Even if you never build a Node.js server, you're using Node.js constantly.

Running your first Node.js code

Once Node.js is installed, you can run any JavaScript file with the node command:

hello.js
console.log('Hello from Node.js');

const now = new Date();
console.log(`Today is ${now.toDateString()}`);
Terminal
node hello.js
# Hello from Node.js
# Today is Fri Jun 27 2025

That's it — JavaScript running on your machine with no browser involved.

package.json — the project manifest

Every Node.js project has a package.json file. It describes your project — its name, version, what packages it depends on, and scripts you can run.

package.json
{
  "name": "my-app",
  "version": "1.0.0",
  "description": "My first Node.js project",
  "main": "index.js",
  "scripts": {
    "start": "node index.js",
    "dev": "node --watch index.js"
  },
  "dependencies": {
    "express": "^4.18.2",
    "dotenv": "^16.3.1"
  }
}

Create one from scratch by running npm init -y in your project folder. The -y flag skips the interactive questions and uses defaults.

npm — the package manager

npm (Node Package Manager) comes with Node.js. It lets you install packages — libraries other developers wrote that you can use in your code.

Common npm commands
# Install a package (adds it to dependencies in package.json)
npm install express

# Install a dev-only package (not needed in production)
npm install --save-dev nodemon

# Install all packages listed in package.json
npm install

# Run a script from package.json
npm run dev
npm start

node_modules — what it is and why it's huge

When you run npm install, all your packages are downloaded into a node_modules folder. This folder can be enormous — hundreds of megabytes — even for small projects.

Why? Because every package you install has its own dependencies, which have their own dependencies, and so on. You install express and end up with dozens of packages in node_modules.

Never commit node_modules to Git. Add it to .gitignore. Anyone who clones your project runs npm install to recreate it. The package.json and package-lock.json files contain everything npm needs to recreate the exact same folder.

Dependencies vs devDependencies

Your package.json has two dependency sections:

package.json — two kinds of dependencies
{
  "dependencies": {
    "express": "^4.18.2",    // Needed to RUN the app
    "dotenv": "^16.3.1"     // Needed to RUN the app
  },
  "devDependencies": {
    "nodemon": "^3.0.1",    // Only needed DURING development
    "eslint": "^8.0.0"      // Only needed DURING development
  }
}

dependencies are installed in both development and production. devDependencies are only installed in development — when you deploy, they're skipped. This keeps production images smaller.

Install to devDependencies with the -D flag: npm install -D nodemon

Semver — version numbers explained

Package versions follow Semver (Semantic Versioning): MAJOR.MINOR.PATCH

  • PATCH (e.g., 4.18.2 → 4.18.3) — bug fixes, backwards compatible
  • MINOR (e.g., 4.18.2 → 4.19.0) — new features, backwards compatible
  • MAJOR (e.g., 4.18.2 → 5.0.0) — breaking changes, may require code updates

In package.json, version prefixes control what updates are allowed:

  • ^4.18.2 — install any 4.x.x that's ≥ 4.18.2 (allows minor + patch updates)
  • ~4.18.2 — install any 4.18.x that's ≥ 4.18.2 (allows patch updates only)
  • 4.18.2 — exact version only, no updates

package-lock.json

When you run npm install, npm creates a package-lock.json file alongside package.json. This file records the exact version of every package that was installed — including the dependencies of your dependencies.

The lock file guarantees reproducible installs. When a teammate runs npm install, they get the exact same package versions you have — not newer ones that might behave differently.

Always commit package-lock.json to Git. Never manually edit it. If it gets corrupted, delete it and run npm install to regenerate.

npx — run a package without installing it

npx comes with npm and lets you run a package's CLI without installing it globally:

npx examples
# Create a new Vite project (downloads and runs create-vite)
npx create-vite@latest my-app

# Deploy to Cloudflare Pages (uses your local wrangler)
npx wrangler pages deploy .

# Run a locally installed package (from node_modules/.bin)
npx eslint src/

When you see commands starting with npx in documentation, it means: "download and run this package temporarily, or use the locally installed version."

npm scripts

The scripts section of package.json lets you define shortcuts for terminal commands. Run them with npm run [name]:

package.json scripts
{
  "scripts": {
    "start": "node index.js",
    "dev": "nodemon index.js",
    "build": "vite build",
    "lint": "eslint src/",
    "deploy": "npm run build && npx wrangler pages deploy dist"
  }
}

start and test are special — you can run them with just npm start or npm test. All other scripts need npm run in front.

ES Modules vs CommonJS

Node.js has two module systems. Older code uses CommonJS (require). Modern code uses ES Modules (import/export).

CommonJS (legacy, still common)
const express = require('express');
const { readFile } = require('fs/promises');

module.exports = { myFunction };
ES Modules (modern — add "type": "module" to package.json)
import express from 'express';
import { readFile } from 'fs/promises';

export { myFunction };

To use ES Module syntax in Node.js, add "type": "module" to your package.json or use .mjs file extensions. Most modern projects prefer ES Modules — it's the same syntax as browser JavaScript.

Global installs vs local installs

Local install (preferred)
# Installed into node_modules/ of this project only
npm install express

# Run via npx or via scripts in package.json
npx eslint .
Global install (use sparingly)
# Installed globally — available in all projects and terminals
npm install -g wrangler
npm install -g typescript

# Now available as a CLI anywhere
wrangler --version
tsc --version

Global installs are convenient for CLIs you use across many projects (wrangler, TypeScript, git-related tools). But for project dependencies, always install locally — it keeps your project self-contained and version-locked.

Node.js built-in modules

Node.js ships with a set of built-in modules — no npm install needed:

Common built-in modules
import { readFile, writeFile } from 'node:fs/promises'; // File system
import path from 'node:path';                          // File paths
import { createServer } from 'node:http';              // HTTP server
import crypto from 'node:crypto';                      // Hashing, encryption
import os from 'node:os';                              // Operating system info
import { EventEmitter } from 'node:events';            // Event system

The node: prefix is the modern way to import built-ins — it makes it explicit that you're importing from Node.js itself, not an npm package.

See also: APIs — how to call external APIs from your Node.js server, and Backend fundamentals — how Node.js fits into the full stack.