March 26, 20266 min read

Node.js From Scratch: What It Actually Is and How to Start

What Node.js is under the hood, npm basics, CommonJS vs ESM modules, building an HTTP server without Express, the event loop, and when Node is the right tool.

nodejs javascript backend server npm
Ad 336x280

Node.js is not a framework. It's not a language. It's a runtime -- specifically, it's Chrome's V8 JavaScript engine pulled out of the browser and paired with libuv, a C library that handles file system access, networking, and other OS-level stuff that JavaScript was never designed to do.

That's it. Node lets you run JavaScript outside a browser, with access to the file system, network sockets, and OS processes. Everything else -- Express, Fastify, Nest, all the npm packages -- is community code built on top.

Installing and Running

Install from nodejs.org (grab the LTS version). Verify it works:

node --version    # v22.x.x or whatever LTS is current
npm --version     # comes bundled with Node

Create a file and run it:

// hello.js
console.log("Running outside the browser. Wild.");
node hello.js

That's genuinely all there is to getting started. No compilation, no build step, no XML config files.

npm: The Package Manager

npm (Node Package Manager) does two things: manages dependencies for your project and hosts the npm registry (the largest package registry in any language ecosystem).

npm init -y                    # creates package.json with defaults
npm install express            # adds express to node_modules/
npm install -D typescript      # -D = devDependency (not shipped to production)
package.json is your project manifest. It lists dependencies, scripts, and metadata. node_modules/ is where packages live locally. It gets huge. Never commit it to git -- add it to .gitignore. package-lock.json pins exact versions so every developer and CI server gets identical dependencies. Commit this file.

npm scripts

{
  "scripts": {
    "start": "node server.js",
    "dev": "node --watch server.js",
    "test": "node --test"
  }
}
npm run dev    # runs the "dev" script
npm start      # "start" is special, doesn't need "run"
npm test       # "test" is also special

Node 18+ has --watch built in, which restarts on file changes. You don't strictly need nodemon anymore, though plenty of projects still use it.

Modules: CommonJS vs ESM

Node historically used CommonJS (CJS):

// math.js (CommonJS)
function add(a, b) { return a + b; }
module.exports = { add };

// app.js
const { add } = require('./math');
console.log(add(2, 3));

The web standardized on ES Modules (ESM), and Node supports them too:

// math.js (ESM)
export function add(a, b) { return a + b; }

// app.js
import { add } from './math.js';
console.log(add(2, 3));

To use ESM in Node, either:


  • Add "type": "module" to your package.json, or

  • Use .mjs file extensions


Most new projects use ESM. Older packages and tutorials still use require(). You'll encounter both. The key difference beyond syntax: ESM is statically analyzable (imports are resolved before execution), CJS is dynamic (require() can go inside an if block). ESM also has top-level await, which CJS doesn't.

Building an HTTP Server (No Framework)

Node's standard library includes http, which can handle raw HTTP requests:

import { createServer } from 'node:http';

const server = createServer((req, res) => {
const url = new URL(req.url, http://${req.headers.host});

if (url.pathname === '/' && req.method === 'GET') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ message: 'Hello from Node' }));
} else if (url.pathname === '/health') {
res.writeHead(200);
res.end('OK');
} else {
res.writeHead(404);
res.end('Not found');
}
});

server.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});

Run it with node server.js, hit http://localhost:3000 in your browser. You just built an HTTP server in 20 lines.

You'd never build a production API this way -- manually parsing routes, handling request bodies, managing middleware -- that's what frameworks are for. But knowing that the raw http module exists, and that Express is just a layer on top of it, helps you understand what's actually happening when things go wrong.

The Event Loop

If you know JavaScript in the browser, you know the event loop concept: callbacks, promises, and setTimeout don't block execution. Node works the same way, but the specifics are slightly different.

Node's event loop (powered by libuv) has phases:

  1. Timers -- runs setTimeout and setInterval callbacks
  2. Pending callbacks -- system-level callbacks (TCP errors, etc.)
  3. Poll -- retrieves new I/O events, executes I/O callbacks
  4. Check -- runs setImmediate callbacks
  5. Close -- cleanup callbacks (socket.on('close', ...))
Microtasks (Promise.then, process.nextTick) run between phases, with process.nextTick having priority over promise callbacks.

The practical takeaway: Node is single-threaded for your JavaScript code but uses a thread pool (via libuv) for I/O operations like file reads, DNS lookups, and crypto. Your code never blocks on I/O -- it hands the operation to the system, continues executing, and gets notified via callback when the result is ready.

This is why Node handles thousands of concurrent connections efficiently. Each connection isn't a thread -- it's just a callback waiting to fire.

When Node Shines (and When It Doesn't)

Node is great for:
  • REST APIs and GraphQL servers
  • Real-time applications (WebSockets, chat, live dashboards)
  • I/O-heavy services (database queries, file processing, API aggregation)
  • CLI tools
  • Build tooling (Webpack, Vite, esbuild are all Node-based)
  • Serverless functions
Node struggles with:
  • CPU-intensive computation (image processing, video encoding, heavy math). The single thread gets blocked and can't handle other requests. Worker threads help, but if CPU work is your primary job, Python, Go, or Rust are better choices.
  • Heavy parallel computation. Node's concurrency model is concurrent I/O, not parallel CPU work.
The workaround for CPU-heavy tasks is worker_threads, which spawn actual OS threads:
import { Worker } from 'node:worker_threads';

const worker = new Worker('./heavy-computation.js', {
workerData: { input: largeDataset }
});

worker.on('message', (result) => {
console.log('Computation done:', result);
});

But if most of your app is CPU-bound, you're fighting against Node's design. Use the right tool.

The Ecosystem

You'll quickly encounter these frameworks and need to pick one:

  • Express -- the original. Minimal, middleware-based, massive ecosystem. Showing its age (no async error handling by default until v5) but still the most popular and most documented.
  • Fastify -- faster than Express, built-in schema validation, better TypeScript support, plugin architecture. The "modern Express" pick.
  • Nest.js -- opinionated, Angular-inspired, heavy use of decorators and dependency injection. Good for large teams that want enforced structure. Overkill for small projects.
  • Hono -- lightweight, runs everywhere (Node, Deno, Bun, Cloudflare Workers). Great for edge functions and serverless.
For a first project, Express or Fastify. Express has more tutorials and Stack Overflow answers. Fastify has better defaults and performance. You can't go wrong with either.

Start Building

The fastest way to get comfortable with Node is to build a small API. Pick something simple -- a todo list, a URL shortener, a bookmark manager -- and build it with the raw http module first. Then refactor it with Express or Fastify and feel the difference. CodeUp has JavaScript and backend challenges that'll get you writing server-side code in the browser before you set up a local project. Once the concepts click, move to your own machine and build something real.

Ad 728x90