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:
console.log('Hello from Node.js');
const now = new Date();
console.log(`Today is ${now.toDateString()}`);node hello.js
# Hello from Node.js
# Today is Fri Jun 27 2025That'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.
{
"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.
# 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 startnode_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.
Dependencies vs devDependencies
Your package.json has two dependency sections:
{
"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.
npx — run a package without installing it
npx comes with npm and lets you run a package's CLI without installing it globally:
# 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]:
{
"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).
const express = require('express');
const { readFile } = require('fs/promises');
module.exports = { myFunction };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
# Installed into node_modules/ of this project only
npm install express
# Run via npx or via scripts in package.json
npx eslint .# 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 --versionGlobal 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:
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 systemThe 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.