Node.js Roadmap

Node.js runs JavaScript on the server. It is used for APIs, real-time services, command line tools, build systems and the back ends of many web and mobile apps. Companies hire for it often, partly because front-end developers can move to the back end without learning a new language.

This roadmap takes you from running your first script to operating a service in production. Each stage lists topics in learning order, explains each one in plain terms, and gives you one exercise. After the stages come projects, interview questions with model answers, common mistakes and a readiness checklist.

By the end you should be able to explain the event loop, write asynchronous code without surprises, build and secure an HTTP API, talk to a database, test your work, and say how you would run and monitor it.

Level
Beginner to advanced
Time
10-14 weeks at about 8 hours a week
Scope
56 topics in 10 stages
Before you start
JavaScript Roadmap fundamentals: functions, objects, arrays, closures and classes; Comfort with a terminal and a code editor; Basic understanding of HTTP requests and responses; Optional: Git and GitHub Roadmap for the projects

What you will be able to do

  • Explain how the event loop schedules callbacks, promises and timers, and predict the order of output
  • Organise code into ES modules and manage dependencies with npm and semantic versioning
  • Process files and large data with streams without loading everything into memory
  • Build, validate and secure a REST API with Express, and explain when fastify, Hono or NestJS fit better
  • Store data with a driver, query builder or ORM and avoid common problems such as N+1 queries
  • Write unit, integration and end-to-end tests and run them in continuous integration
  • Find memory leaks and blocked event loops, and use worker threads for CPU-heavy work
  • Deploy a service in a container with graceful shutdown, logging and health checks

The roadmap at a glance

  1. Stage 1Getting Started with Node.jsBeginner · 1 week · 5 topics
  2. Stage 2Modules, Packages and npmBeginner · 1-2 weeks · 7 topics
  3. Stage 3The process Object and Asynchronous BasicsBeginner · 2 weeks · 5 topics
  4. Stage 4The Event Loop, Timers and EventsIntermediate · 1-2 weeks · 4 topics
  5. Stage 5Files, Streams and HTTPIntermediate · 2 weeks · 6 topics
  6. Stage 6Errors, Debugging and Command Line ToolsIntermediate · 2 weeks · 8 topics
  7. Stage 7Building Web ServersIntermediate · 2-3 weeks · 6 topics
  8. Stage 8DatabasesIntermediate · 2 weeks · 5 topics
  9. Stage 9TestingIntermediate · 1-2 weeks · 4 topics
  10. Stage 10Concurrency, Performance and ProductionAdvanced · 2-3 weeks · 6 topics

Stage 1 Getting Started with Node.js

Beginner1 week5 topics

Goal. Explain what Node.js is, run a script, and say where it differs from running JavaScript in a browser.

What is Node.js? Core

Node.js is a runtime that lets you run JavaScript outside a browser. It embeds the V8 engine, the same engine that powers Chrome, and adds a layer of C++ and JavaScript that gives scripts access to files, sockets, timers, processes and the operating system. Introduction to Node.js usually stops at that definition, but the useful idea is the programming model: your code runs on one main thread, and slow work such as disk and network access is handed to the operating system or to a small pool of helper threads.

A Node.js program is just a JavaScript file. Run it with node app.js and it ends when there is nothing left to do, or keeps running while a server or timer is still active.

Practice. Run node -e "console.log(process.version)" and write down what each part of the command does.

Why use Node.js? Core

Teams pick Node.js for a few concrete reasons. One language can serve both the browser and the server, which makes hiring and code sharing easier. The event-driven model handles many idle connections cheaply, so it suits APIs, chat, streaming and proxies that spend most of their time waiting for other systems. The package registry is the largest of any language, and start-up time is short, which helps with serverless functions and command line tools.

It is a weaker choice for long CPU-bound work such as video encoding or heavy number crunching, because that work blocks the main thread unless you move it to a worker. Be ready to state both sides in an interview. If you need typed code, read TypeScript Roadmap alongside this roadmap.

Practice. List three services you would build with Node.js and one you would build with something else, with a reason for each.

History of Node.js Optional

Ryan Dahl created Node.js in 2009 after becoming frustrated with servers that blocked a whole thread on each request. He combined V8 with an event loop and non-blocking input and output. The npm registry followed soon after and became the main reason for the platform's growth.

A few moments are worth knowing. In 2014 a group of contributors forked the project as io.js over governance disputes, and the two merged again in 2015 under the Node.js Foundation. Release lines now follow a regular schedule: even-numbered versions become long-term support releases, and odd-numbered ones are short-lived. Production systems should use an active LTS line. Newer releases have added built-in features that once needed packages, such as fetch, a test runner and a watch mode.

Practice. Open the release schedule for Node.js and write down which line is currently active LTS.

Node.js vs Browser Core

Both run JavaScript, but the environments differ. The browser gives you window, the DOM, localStorage and security rules that stop pages reading your disk. Node.js has no DOM and no window. It gives you process, the file system, child processes and network servers, and it trusts the code it runs.

Other differences matter in practice. Node.js has a global called globalThis rather than window. Modules come from files and the node_modules folder, not script tags. You control the runtime version, so you can use new language features straight away, while browser code must support whatever users have installed.

Practice. Write a script that prints typeof window and typeof process, then run it in Node.js and in a browser console.

Running Node.js Code Core

There are three common ways to run code. The REPL, started by typing node, evaluates one line at a time and is good for trying small ideas. node -e "code" runs a short string. Running Scripts is the normal route: node file.js executes a file from top to bottom.

Package scripts live in package.json and run with npm run name, which also adds node_modules/.bin to the path.

For development you want automatic restarts. Since Node.js 18.11 the --watch flag restarts a script when imported files change. Before that, Nodemon was the standard tool, and it is still common because it has more options, such as watching extra folders and extensions.

Practice. Create a script that prints a timestamp, run it with node --watch, and edit the file to see it restart.

Stage 2 Modules, Packages and npm

Beginner1-2 weeks7 topics

Goal. Split code into modules, install and publish packages, and read a package.json with confidence.

Node.js Modules Core

A module is a file whose contents are private until it exports something. Creating & Importing a module is the first skill to master: export what other files need, import it where it is used, and keep everything else hidden.

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

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

Node.js also resolves bare names such as express by looking in node_modules folders from the current directory upwards, and relative paths such as ./math.js from the importing file. Built-in modules can be imported with a node: prefix, for example node:fs, which makes clear that the name is not a package.

Practice. Split a small program into three modules and draw which module imports which.

CommonJS vs ESM Core

Node.js supports two module systems. CommonJS is the original one, using require() and module.exports. ESM is the standard JavaScript system, using import and export. New projects should prefer ESM.

The practical differences:

  • ESM is chosen by the .mjs extension or by "type": "module" in package.json. Without it, .js files are CommonJS.
  • ESM imports are static and asynchronous, which allows top-level await. require() is synchronous and can be called anywhere.
  • ESM needs file extensions in relative imports and has no __dirname; use import.meta.dirname in current versions or fileURLToPath(import.meta.url).
  • ESM can import CommonJS packages. Importing ESM from CommonJS has been awkward, though recent Node.js releases allow require() of synchronous ES modules.

Practice. Convert a small CommonJS script to ESM and list every line you had to change.

Nodejs core modules Core

Node.js ships with built-in modules that need no installation. The ones every developer meets are fs for files, path for file paths, http and https for servers and clients, os for system information, events for emitters, stream for streaming data, crypto for hashing and random values, url, util, zlib for compression, child_process, worker_threads and assert.

Two names look like keywords but are not. The global keyword is not a thing in modern code: the global object is globalThis, and assigning to it is rarely a good idea. __dirname and __filename give the folder and the file of the current CommonJS module. They are the safe base for building paths to files that sit next to your code, rather than relying on the current working directory.

Practice. Use os, path and crypto to print the home folder, a joined path and a random UUID.

npm Core

npm is the package manager that comes with Node.js. npm init -y creates a package.json. npm install express adds a dependency, and npm install -D vitest adds a development-only one. The lock file, package-lock.json, records exact versions and should be committed so that every machine installs the same tree. In CI use npm ci, which installs exactly from the lock file and fails if it disagrees with package.json.

Local Installation puts packages in the project's node_modules. Global Installation, with npm install -g, is for tools you run from anywhere, but pinning them per project is usually better. npx tool runs a package binary, downloading it temporarily if needed. Updating Packages is done with npm outdated to see what is behind and npm update to move within allowed ranges.

Practice. Create a project, install one runtime and one dev dependency, then run npm outdated and npm audit.

Semantic Versioning Core

Semantic Versioning writes a release as MAJOR.MINOR.PATCH. A patch fixes bugs, a minor adds features without breaking existing use, and a major may break callers. Packages on npm are expected to follow this.

The ranges in package.json rely on it. ^1.4.2 accepts any 1.x.y at or above 1.4.2, ~1.4.2 accepts only 1.4.x, and a bare 1.4.2 pins one version. For versions below 1.0.0, a caret is stricter, because ^0.3.1 only allows 0.3.x.

A range protects you only as far as the maintainers keep the promise, which is why the lock file matters. When you publish, bump the version with npm version patch|minor|major.

Practice. Explain, for each of ^2.3.1, ~2.3.1 and 2.x, which versions npm would accept.

Creating Packages Recommended

Publishing your own package teaches how the ecosystem fits together. Start with a clear name, version, description and license in package.json. The main field or the newer exports map says which file consumers get, and exports also lets you offer separate ESM and CommonJS builds and hide internal files. The files list controls what goes into the tarball; check it with npm pack --dry-run. A bin field turns a script into a command, with a #!/usr/bin/env node first line.

Publish with npm publish, after logging in. Never publish secrets: check the packed file list first.

Practice. Create a tiny package with a bin command, pack it, and install the tarball in another folder.

npm workspaces Recommended

Workspaces let one repository hold several packages that share one install. Add a "workspaces": ["packages/*"] array to the root package.json, and npm links the packages into the root node_modules, so one package can depend on a sibling by name without publishing it.

Useful commands are npm install at the root, npm run test --workspaces to run a script everywhere, and npm run build -w packages/api to target one package.

A monorepo adds problems that npm does not solve alone, such as build ordering, caching and releasing only what changed. Tools such as Turborepo and Nx handle those, but plain workspaces are enough for small repositories.

Practice. Create a workspace with a shared library and an app that imports it, and run a script in both.

Stage 3 The process Object and Asynchronous Basics

Beginner2 weeks5 topics

Goal. Read input from the environment, and write asynchronous code with callbacks, promises and async functions.

The process object Core

The global process object describes the running program. Four properties come up constantly.

  • process.argv is an array of command line arguments. The first two entries are the Node.js binary and the script, so user arguments start at index 2. For anything beyond trivial parsing use util.parseArgs.
  • process.env holds environment variables as strings. Read configuration and secrets from it, and convert numbers yourself: Number(process.env.PORT ?? 3000).
  • process.cwd() returns the directory the process was started from, which is not the same as the script's folder.
  • process.stdin and process.stdout are streams for input and output. console.log writes to stdout, and console.error to stderr. Process stdout supports write() without adding a newline, which is useful for progress messages.

Practice. Write a script that reads a name from process.argv and a greeting from process.env, and prints the combination.

Async Programming Core

Reading a file or calling an API takes time. If the program waited on the main thread, nothing else could run. Node.js starts the operation and carries on, then runs a function when the result arrives. That is Async Programming in Node.js, and nearly all of its input and output works this way.

There are three styles that you will meet in real code: callbacks, promises and async/await. Most built-in modules now offer promise versions, such as node:fs/promises.

The important habit is to think about ordering. Code after an asynchronous call runs before the result is ready, so anything that needs the result must be inside the callback, after the await, or in a .then.

Practice. Predict the print order of a script that logs before, inside and after a setTimeout of zero, then run it.

Callbacks Core

A callback is a function you pass to another function to be called when work finishes. Node.js uses the error-first convention: the first argument is an error or null, and the result follows.

import { readFile } from 'node:fs';
readFile('data.txt', 'utf8', (err, text) => {
  if (err) return console.error(err.message);
  console.log(text.length);
});

Never ignore err, and never call a callback twice. Callbacks nest when steps depend on each other, which produces the pyramid shape known as callback hell. Naming the functions helps a little. Promises solve the problem properly. util.promisify wraps an error-first function into one that returns a promise.

Practice. Read two files in sequence with callbacks, then rewrite the code with util.promisify.

Promises Core

A promise is a placeholder for a result that is not ready yet. It is pending, then either fulfilled with a value or rejected with an error, and it never changes again. You attach handlers with .then() and .catch(), and each returns a new promise, which is what lets you chain steps flat instead of nesting.

The static helpers matter in interviews. Promise.all waits for every promise and rejects as soon as one fails. Promise.allSettled waits for all and reports each outcome. Promise.race settles with the first to finish, and Promise.any fulfils with the first success. Promise.withResolvers() is a newer helper for code that needs to settle a promise from outside.

An unhandled rejection stops the process in current Node.js versions, so always end a chain with a .catch or use try/catch.

Practice. Fetch three URLs with Promise.all, then change one to fail and compare Promise.all with allSettled.

Async/Await Core

async functions return a promise, and await pauses the function until a promise settles, so asynchronous code reads like ordinary sequential code. Errors become exceptions that try/catch can handle.

async function loadUser(id) {
  try {
    const res = await fetch(`https://api.example.test/users/${id}`);
    if (!res.ok) throw new Error('HTTP ' + res.status);
    return await res.json();
  } catch (err) {
    console.error('load failed', err);
    throw err;
  }
}

Awaiting inside a loop runs the steps one after another. When tasks are independent, start them together and await Promise.all. Top-level await works in ES modules. Forgetting await is a common bug: you get a pending promise instead of a value.

Practice. Fetch a list of ten items sequentially, then in parallel, and compare the time taken.

Stage 4 The Event Loop, Timers and Events

Intermediate1-2 weeks4 topics

Goal. Predict the order in which asynchronous code runs and use event emitters to decouple parts of a program.

Event Loop Core

The Event Loop is the mechanism that lets one thread handle many tasks. When the call stack is empty, Node.js takes the next ready callback from a queue and runs it. The loop moves through phases in a fixed order: timers (due setTimeout and setInterval callbacks), pending callbacks, poll (waiting for and handling I/O), check (setImmediate), and close callbacks.

Between each callback Node.js drains two microtask queues: first process.nextTick callbacks, then promise reactions. That is why a resolved promise handler runs before a timer set to zero.

Real file and network operations run in the operating system or in libuv's thread pool, which has four threads by default. The consequence to remember: any long synchronous loop blocks everything else, including incoming requests.

Practice. Write a script mixing setTimeout, setImmediate, a promise and process.nextTick, predict the order, then verify.

setTimeout, setInterval and timers Core

setTimeout(fn, ms) schedules one call after at least ms milliseconds. setInterval(fn, ms) repeats it. The delay is a minimum, not a promise, because the callback must wait for the stack to clear and earlier work to finish. Both return a handle that you pass to clearTimeout or clearInterval.

Intervals can pile up if the callback takes longer than the delay. When each run must finish before the next starts, schedule the next one with setTimeout at the end of the function, or use an async loop with await setTimeout from node:timers/promises.

Calling .unref() on a timer handle tells Node.js not to stay alive just for it, which is useful for housekeeping tasks.

Practice. Build a polling function that checks a URL every five seconds without overlapping runs and stops after three failures.

process.nextTick() and setImmediate Recommended

These two are often confused. process.nextTick(fn) runs fn right after the current operation, before the event loop continues and before promise callbacks. It is processed in its own queue, so a recursive nextTick can starve the loop and block all I/O. setImmediate(fn) runs in the check phase, after the current poll phase, so it lets I/O callbacks go first.

Rules of thumb:

  • Use setImmediate to defer work without starving I/O.
  • Use queueMicrotask or a promise for ordinary deferral that follows promise semantics.
  • Use process.nextTick mainly when you must make a callback API consistently asynchronous, for example emitting an event after the caller has attached a listener.

Practice. Show with code that a long chain of nextTick calls delays a timer, and fix it with setImmediate.

Event Emitter Core

EventEmitter from node:events implements the observer pattern. Objects call emit('name', data) and other code subscribes with on('name', handler). Many core classes, including streams, servers and sockets, are emitters.

import { EventEmitter } from 'node:events';
const bus = new EventEmitter();
bus.once('ready', () => console.log('started'));
bus.on('job', (id) => console.log('job', id));
bus.emit('ready');
bus.emit('job', 7);

Listeners run synchronously in the order they were added. Emitting error with no listener throws and can crash the process, so always add an error handler. Remove listeners you no longer need to avoid leaks, because Node.js warns when more than ten are added to one event. events.once(emitter, 'name') returns a promise for a single event.

Practice. Build a small job queue class that extends EventEmitter and emits start, done and error events.

Stage 5 Files, Streams and HTTP

Intermediate2 weeks6 topics

Goal. Work with files and paths safely, process large data as streams, and make and serve HTTP requests.

fs module and Working with Files Core

The fs module reads, writes, renames and deletes files and folders. It has three flavours: callbacks, synchronous (readFileSync) and promises through node:fs/promises. Prefer the promise form in application code, and keep sync calls to start-up scripts.

import { readFile, writeFile, mkdir } from 'node:fs/promises';
await mkdir('out', { recursive: true });
await writeFile('out/log.txt', 'hello
', { flag: 'a' });
const text = await readFile('out/log.txt', 'utf8');

Working with Files covers more than reading text: stat gives size and type, readdir lists a folder, rm with { recursive: true } deletes trees, and access checks permissions.

Practice. Write a script that lists a folder recursively and prints the ten largest files.

path module Core

Never build file paths by joining strings with slashes, since Windows uses backslashes. The path module does it correctly: path.join('a', 'b', 'c.txt'), path.resolve('x') for an absolute path, path.basename, path.dirname, path.extname and path.parse.

Combine it with __dirname or import.meta.dirname to locate files relative to the source file rather than the current working directory.

Security point: when a path comes from a user, joining it to a base folder does not stop ../ escapes. Resolve the final path and check that it still starts with the intended base, or reject names containing separators. This mistake is the cause of path traversal bugs in file servers.

Practice. Write a function that safely resolves a user-supplied file name inside an uploads folder and rejects traversal attempts.

File helper packages Optional

A few packages fill gaps in the built-ins, although newer Node.js versions have narrowed them.

  • fs-extra adds conveniences such as copy, ensureDir, remove and readJson, and returns promises.
  • glob finds files with patterns such as src/**/*.test.js. globby wraps it with extra features, such as respecting .gitignore. Node.js also has fs.glob in recent releases.
  • chokidar watches files for changes more reliably across platforms than fs.watch, and underlies many build tools.

Practice. Use glob to find every markdown file in a project and chokidar to log whenever one changes.

Streams Core

A stream processes data piece by piece instead of loading it all. That keeps memory flat when you copy a two-gigabyte file or pass a download to a client. There are four kinds: readable, writable, duplex (both, like a socket) and transform (modifies data in transit, like gzip).

import { createReadStream, createWriteStream } from 'node:fs';
import { pipeline } from 'node:stream/promises';
import { createGzip } from 'node:zlib';
await pipeline(
  createReadStream('big.log'),
  createGzip(),
  createWriteStream('big.log.gz')
);

Use pipeline rather than .pipe(), because it forwards errors and cleans up every stream. Backpressure is the idea that a slow consumer must be able to slow the producer. pipeline and write() returning false plus a drain event handle it.

Practice. Build a pipeline that reads a large CSV, transforms each line, and writes the result without loading the whole file.

http module Core

The http module is the base under every Node.js web framework. A server is a function that receives a request and fills a response.

import http from 'node:http';
const server = http.createServer((req, res) => {
  if (req.method === 'GET' && req.url === '/health') {
    res.writeHead(200, { 'content-type': 'application/json' });
    return res.end(JSON.stringify({ ok: true }));
  }
  res.writeHead(404).end();
});
server.listen(3000);

The request is a readable stream, so a body arrives in chunks that you must collect and parse. The response is a writable stream. You manage routing, headers, status codes, body parsing and errors yourself, which is the exact work frameworks automate. Writing one small server by hand makes Express and fastify much easier to understand. For protocol background see API Design Roadmap.

Practice. Write a server without a framework that supports GET and POST on a /notes route, holding notes in memory.

fetch, Axios, Got and Ky Recommended

To call other services you need an HTTP client. Node.js 18 and later include the standard fetch, so a dependency is optional. Remember that fetch does not reject on a 404 or 500; check res.ok. It also has no default timeout, so pass AbortSignal.timeout(5000).

Libraries add convenience on top:

  • Axios gives automatic JSON handling, interceptors and error throwing on bad status codes. It works in the browser too.
  • Got is Node.js-focused, with retries, hooks and streaming support.
  • Ky is a small wrapper around fetch with retries and timeouts, used in browsers and Node.js.

Set a timeout on every call and retry only idempotent requests.

Practice. Write a helper around fetch that adds a timeout, checks the status and retries twice on network errors.

Stage 6 Errors, Debugging and Command Line Tools

Intermediate2 weeks8 topics

Goal. Handle every kind of error deliberately, debug a running program, and build a command line application.

Error Handling Core

Good error handling starts with knowing the kinds. JavaScript Errors are objects of the Error class and its subclasses such as TypeError and RangeError. System Errors come from the operating system, such as a missing file, and carry a code like ENOENT or EADDRINUSE. User Specified Errors are ones you throw yourself, ideally from a custom class with a clear name and extra fields.

class NotFoundError extends Error {
  constructor(what) {
    super(what + ' not found');
    this.name = 'NotFoundError';
    this.status = 404;
  }
}

Throw Error objects, never strings, because only errors carry a stack. Use the cause option to wrap a lower-level error without losing it: new Error('load failed', { cause: err }). Assertion Errors come from node:assert and signal that a condition you believed was true was not.

Practice. Create a custom error class with a status code and throw it from a function that wraps a failing file read, keeping the cause.

Stack Trace Core

A stack trace lists the functions that were active when an error was created, newest first, with file names and line numbers. Reading one quickly is a core debugging skill: start at the top, find the first line in your own code rather than in node:internal or node_modules, and look at what that line was doing.

Asynchronous code shortens traces, because the original caller is no longer on the stack after an await. Current V8 adds async frames for await chains, which helps.

Practice. Trigger an error three function calls deep inside an async function and annotate each line of the trace.

Async errors and Uncaught Exceptions Core

Synchronous try/catch cannot catch an error thrown inside a callback that runs later. For callbacks use the error argument; for promises use .catch or try/catch around await. An async function that throws returns a rejected promise, so the caller must handle it.

Errors nobody handles reach process-level events. unhandledRejection fires for a rejected promise with no handler, and since Node.js 15 the default is to crash. uncaughtException fires for a thrown error that nothing caught. Treat these as a last resort for logging, not recovery: after an Uncaught Exceptions event the application state is unknown, so log, flush, and exit with a non-zero code, and let a supervisor restart the process.

In Express 4, an error thrown inside an async route is not passed to the error middleware unless you catch it; Express 5 handles rejected promises for you.

Practice. Register unhandledRejection and uncaughtException handlers that log and exit, then trigger each one on purpose.

Exiting and Exit Codes Recommended

When a program finishes, it returns a number to the shell. Exiting / Exit Codes follow a convention: 0 means success and any other value means failure. CI systems and scripts read it to decide whether to continue.

process.exit(code) stops at once, which can cut off pending writes to files or pipes. Prefer setting process.exitCode = 1 and letting the loop drain, and call process.exit only after cleanup. Listen for SIGINT (Ctrl+C) and SIGTERM (sent by container platforms on shutdown) to close servers and databases first. Uncaught exceptions exit with code 1.

process.on('SIGTERM', () => {
  server.close(() => process.exit(0));
});

A server that ignores SIGTERM is killed after a grace period, dropping in-flight requests.

Practice. Add a SIGINT handler to a script that finishes the current task, prints a summary and exits with the right code.

Debugging Core

Start with the cheap tools. console.log and console.table work, and console.time measures a section. When that is not enough, attach a real debugger. Running node --inspect app.js opens a debugging port that Chrome DevTools or VS Code can connect to, and --inspect-brk pauses on the first line. Using Debugger features means setting breakpoints, stepping through lines, inspecting variables and watching expressions, and using conditional breakpoints for loops.

The debugger; statement pauses execution when a debugger is attached. For tests, VS Code can launch and debug a test file directly. For a stuck process, a CPU profile or heap snapshot from the inspector shows where time or memory goes.

Practice. Debug a small server with node --inspect, set a conditional breakpoint inside a route and inspect the request object.

Node.js Logging Core

Logs are how you understand a running service. Start with levels: error, warn, info and debug, so that production can run at info and you can switch to debug when investigating. Write structured JSON lines with a timestamp, level, message and context such as a request id, because machines search these far better than free text.

Winston is a configurable logger with transports for the console, files and external services. Morgan is HTTP request logging middleware for Express, producing one line per request. Pino is another popular choice that focuses on speed and JSON output. Libraries such as these write to stdout, and the platform collects it; avoid writing log files from the app inside containers.

Practice. Add JSON logging with a request id to an HTTP server and log one info line and one error per request.

Command Line Applications Recommended

Node.js is a good fit for command line tools because it starts fast and has the whole package registry behind it. A tool is a script with a #!/usr/bin/env node first line, a bin entry in package.json, and well-chosen exit codes.

Commander.js is the common choice for parsing arguments. It handles commands, options, defaults, help text and version output.

import { Command } from 'commander';
const program = new Command();
program.name('todo').version('1.0.0');
program.command('add <text>').option('-p, --priority <n>', 'priority', '1')
  .action((text, opts) => console.log('added', text, opts.priority));
program.parse();

Send results to stdout and diagnostics to stderr, so users can pipe the output.

Practice. Build a todo command line tool with add, list and done commands that stores items in a JSON file.

Interactive CLI packages Optional

Several small packages improve the experience of a terminal tool.

  • Inquirer Package and the prompts package ask questions interactively: text input, yes or no, lists and checkboxes. prompts is smaller; Inquirer has more question types.
  • The chalk package adds colour and styles to text. It respects terminals without colour, but check the NO_COLOR convention and do not rely on colour alone to carry meaning.
  • The figlet package turns text into large ASCII art banners, which is decoration for welcome screens.
  • cli-progress draws progress bars for long operations.

Recent Node.js versions include util.styleText and util.parseArgs, so small tools may not need chalk or an argument parser at all.

Practice. Add an interactive mode to your todo tool that prompts for text and priority and prints coloured output only on a terminal.

Stage 7 Building Web Servers

Intermediate2-3 weeks6 topics

Goal. Build, structure and secure an HTTP API using a framework, and choose between the main options.

Express.js Core

Express.js has long been the default choice for web servers in Node.js. It layers routing, middleware and response helpers over the http module.

import express from 'express';
const app = express();
app.use(express.json());
app.get('/users/:id', (req, res) => {
  res.json({ id: req.params.id });
});
app.use((err, req, res, next) => {
  res.status(500).json({ error: 'Something went wrong' });
});
app.listen(3000);

Middleware are functions with access to the request, the response and next, run in the order registered. They parse bodies, log, check authentication and handle errors. An error handler has four parameters and goes last. Keep route files thin: validate input, call a service function, and send a response. Express 5 is now the default release and passes rejected promises from async handlers to the error handler. Add helmet for secure headers and cors only for origins you trust.

Practice. Build a REST API for notes with validation, an error handler and a health endpoint, split into route and service files.

fastify and Hono Recommended

Newer frameworks make different trade-offs from Express.

fastify is built for speed and structure. It validates requests and serialises responses using JSON Schema, has a plugin system with encapsulation, and ships good logging through Pino. It suits teams that want validation and documentation generated from schemas.

Hono is a small, fast framework written on web-standard Request and Response objects. The same app can run on Node.js, Deno, Bun and edge platforms such as Cloudflare Workers, which makes it popular for edge and serverless work. It has a pleasant router and typed helpers.

Choosing: Express has the biggest ecosystem and the most tutorials. fastify gives structure and performance. Hono gives portability. In an interview, explain that the framework matters less than the principles of routing, middleware, validation and error handling, which carry over.

Practice. Rebuild your notes API in fastify using a JSON Schema for the request body and compare how validation errors look.

NestJS Recommended

NestJS is a TypeScript-first framework for larger server applications. It organises code into modules, controllers (which handle routes), providers (services), and uses decorators and dependency injection, an approach familiar from Angular and Spring.

@Controller('notes')
export class NotesController {
  constructor(private readonly notes: NotesService) {}
  @Get(':id')
  find(@Param('id') id: string) { return this.notes.find(id); }
}

It runs on Express or fastify underneath, and has built-in support for pipes (validation and transformation), guards (authorisation), interceptors, exception filters and testing utilities. See TypeScript Roadmap and Angular Roadmap for the supporting ideas.

Practice. Create a NestJS module with one controller and one service, and add a validation pipe to the create route.

Template Engines Optional

Template Engines produce HTML on the server by combining a template with data. They suit sites that render pages rather than serving JSON to a front end.

  • EJS embeds JavaScript in HTML with tags such as <%= value %>. It is easy to learn.
  • Pug uses indentation instead of closing tags and has features such as mixins and includes.
  • Marko is a component-based language that supports streaming and partial hydration in the browser.

Express connects to an engine with app.set('view engine', 'ejs') and res.render('page', data). Always use the escaping form of the output tag, because unescaped output is the usual route to cross-site scripting.

Practice. Render a list of notes with EJS and prove that a note containing a script tag is escaped.

Authentication with JWT and Passport Core

Authentication proves who a caller is. Two common approaches exist. Server-side sessions store a session id in a cookie and keep data on the server. A JSON Web Token (JWT) is a signed string containing claims such as the user id and an expiry; the server verifies the signature and needs no lookup.

Passport js is middleware that standardises login through "strategies": local username and password, Google and GitHub OAuth, JWT and many more.

Practical rules: hash passwords with a slow algorithm such as argon2 or bcrypt, never store them in plain text, keep access tokens short-lived, store tokens in httpOnly cookies when the client is a browser, and sign with a secret from the environment. A JWT cannot be revoked easily before expiry, so use short lifetimes with refresh tokens, or choose sessions when you need instant logout.

Practice. Add registration and login to your API with hashed passwords and a short-lived JWT, and protect one route.

Securing an API Core

A handful of habits prevent most real incidents.

  • Validate every input with a schema library such as Zod or the framework's own validation, and reject what does not match.
  • Use parameterised queries or an ORM, never string concatenation, to avoid SQL injection.
  • Set secure headers with helmet, limit request body size, and rate-limit login and expensive routes.
  • Keep secrets in environment variables or a secrets manager, never in the repository.
  • Run npm audit, keep dependencies updated, and pin them with a lock file.
  • Return generic error messages to clients and keep details in logs.
  • Configure CORS to allow named origins, not *, for credentialed requests.

Authorisation is separate from authentication: after you know who the user is, check on every request that they may touch that specific record. Missing that check is the most common API flaw.

Practice. Review your API against this list and fix every gap, adding a rate limit to the login route.

Stage 8 Databases

Intermediate2 weeks5 topics

Goal. Connect a Node.js service to relational and document databases and choose between drivers, query builders and ORMs.

What is Database Core

A database stores data durably and lets many clients read and change it safely. Your Node.js process loses everything on restart, so anything that must last belongs in one.

Relational databases such as PostgreSQL and MySQL keep data in tables with a fixed schema, link rows through keys and are queried with SQL. They give transactions, which make a group of changes succeed or fail together. Document databases such as MongoDB store JSON-like documents with a flexible shape, and suit data that is read as a whole. Key-value stores such as Redis keep simple values in memory for caching and queues.

Start with a relational database unless you have a reason not to. The SQL Roadmap roadmap covers queries and design, and MongoDB Roadmap and Redis Roadmap cover the others.

Practice. Write down which database you would pick for a blog, a chat history and a session cache, with one reason each.

Native Drivers Core

A driver is the package that talks the database's wire protocol. Using one directly gives full control and no abstraction cost. For relational data the usual choices are pg for PostgreSQL, mysql2 for MySQL and better-sqlite3 or the built-in node:sqlite for SQLite. Native Drivers (NoSQL) means packages such as mongodb for MongoDB or redis for Redis.

import pg from 'pg';
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });
const { rows } = await pool.query(
  'SELECT id, name FROM users WHERE email = $1', [email]);

Always use a connection pool instead of opening one connection per request, and always pass values as parameters, never inside the SQL string. Close the pool on shutdown. The cost of a native driver is that you write every query and map every row by hand.

Practice. Connect to a local database with a pool, insert a row with parameters, and read it back.

Query builders: Knex Recommended

A query builder lets you compose SQL with JavaScript method calls. Knex is the best known. It produces parameterised queries for several databases, manages connection pools, and includes migrations and seed files.

const adults = await knex('users')
  .where('age', '>=', 18)
  .orderBy('name')
  .select('id', 'name');

You still think in tables and joins, so the SQL you need to know is the SQL you write, just assembled safely. Dynamic filters, such as a search form with optional fields, are much easier to build this way than by concatenating strings. A query builder gives no object mapping, so results are plain rows. Many teams now choose Drizzle or Prisma for new projects, but Knex is common in existing code.

Practice. Write a Knex migration for a posts table and a function that builds a filtered query from optional parameters.

ORMs: Sequelize, TypeORM, Prisma and Drizzle Recommended

An ORM maps tables to objects in code.

  • Sequelize is the long-standing JavaScript ORM, with models defined in code and promise-based queries. TypeScript support is bolted on.
  • TypeORM uses decorators and classes, in the style of Hibernate, and supports both an active record and a repository pattern.
  • Prisma describes the schema in its own file and generates a type-safe client from it. Migrations come built in. It is very popular for its developer experience.
  • Drizzle defines schemas in TypeScript and writes queries that stay close to SQL while giving full types. It is light and has no code generation step.

Benefits: less boilerplate and typed results. Risks: hidden queries, such as the N+1 problem where one query per row loads related data, and awkward complex reports. Learn to log the SQL an ORM produces and drop to raw SQL when it is clearer.

Practice. Model users and posts in Prisma or Drizzle, load each user with their posts, and log the SQL that runs.

Mongoose Recommended

Mongoose is the standard object-document mapper for MongoDB in Node.js. MongoDB itself accepts any shape of document; Mongoose adds a schema with types, defaults and validation, plus models with query methods, middleware hooks and virtual fields.

const userSchema = new mongoose.Schema({
  email: { type: String, required: true, unique: true },
  createdAt: { type: Date, default: Date.now },
});
const User = mongoose.model('User', userSchema);
const user = await User.create({ email: 'a@example.test' });

Points to remember: a unique option builds an index and does not validate by itself. Use lean() when you only need plain objects, since it skips hydration and is faster. Use populate to join references, but know that it runs extra queries. Design documents around how you read them, embedding data that is always read together and referencing data that grows without limit.

Practice. Create a Mongoose model with validation and an index, then query it with filters, sorting and pagination.

Stage 9 Testing

Intermediate1-2 weeks4 topics

Goal. Write automated tests at several levels and run them in continuous integration.

Testing Core

Testing is how you change code without fear. Think in layers. Unit tests check one function or class in isolation and run in milliseconds. Integration tests check that parts work together, for example a route and a real database. End-to-end tests drive the whole application the way a user does and are the slowest and most fragile, so write fewer of them.

Good tests are fast, deterministic and independent. Each test sets up its own data, asserts on behaviour rather than implementation, and gives a failure message that points at the problem. Avoid sleeping for fixed times; wait for conditions.

Replace slow or unpredictable dependencies, such as the clock, the network and payment services, with test doubles: stubs return canned answers, mocks verify calls. Do not mock what you do not own without a contract check, or your tests can pass while production fails.

Practice. Write a list of the unit, integration and end-to-end tests you would write for a login endpoint.

node:test Recommended

Node.js includes a test runner, so a simple project needs no test framework. Import test and assert from the standard library and run the files with node --test.

import test from 'node:test';
import assert from 'node:assert/strict';
import { add } from './math.js';

test('add sums numbers', () => {
  assert.equal(add(2, 3), 5);
});

The runner supports describe and it, hooks such as before and afterEach, mocking through mock, subtests, running tests concurrently, a watch mode and coverage reporting with --experimental-test-coverage in versions where it is still flagged. It is a good default for libraries and small services because there is nothing to install or configure.

Practice. Test a small module with node:test, including a mocked function and a test of an async function that rejects.

Jest and Vitest Core

Jest has been the default Node.js test framework for years. It includes a runner, assertions through expect, mocking, snapshots and coverage in one package, and runs files in parallel. It works best with CommonJS; ESM support needs configuration.

Vitest has a Jest-compatible API but is built on Vite, so it handles ESM and TypeScript out of the box, starts quickly and has a fast watch mode. For new projects it is often the easier choice.

import { describe, it, expect, vi } from 'vitest';
describe('price', () => {
  it('applies discount', () => {
    const log = vi.fn();
    expect(applyDiscount(100, 0.1, log)).toBe(90);
    expect(log).toHaveBeenCalledOnce();
  });
});

Practice. Test an Express route with Supertest and a mocked service, covering one success and one validation failure.

Cypress and Playwright Optional

End-to-end tools control a real browser to test the application as a whole. Cypress runs inside the browser with a friendly interactive runner and time-travel debugging, and is oriented to testing front ends. Playwright, from Microsoft, drives Chromium, Firefox and WebKit from one API, supports several tabs and contexts, and can also test APIs directly.

Both retry assertions automatically until they pass or time out, which removes most flaky waits.

For a Node.js backend developer, the useful skill is knowing what they are for and how to run them in a pipeline, not mastering every command.

Practice. Write one Playwright test that logs in to a demo app and checks that a protected page loads.

Stage 10 Concurrency, Performance and Production

Advanced2-3 weeks6 topics

Goal. Use more than one core safely, find memory and performance problems, and run a Node.js service reliably in production.

Worker Threads Core

Node.js JavaScript runs on one thread, so a CPU-heavy task such as image resizing or hashing a large file blocks every request. Nodejs Threads solve this: worker_threads runs JavaScript in separate threads, each with its own event loop and memory, communicating by messages.

import { Worker, isMainThread, parentPort, workerData } from 'node:worker_threads';
if (isMainThread) {
  const w = new Worker(new URL(import.meta.url), { workerData: 40 });
  w.on('message', (n) => console.log('result', n));
} else {
  const fib = (n) => (n < 2 ? n : fib(n - 1) + fib(n - 2));
  parentPort.postMessage(fib(workerData));
}

Data is copied between threads, unless you transfer an ArrayBuffer or share memory with SharedArrayBuffer and Atomics. Threads do not help with I/O, which is already asynchronous.

Practice. Move a CPU-heavy function into a worker and show that the server still responds to health checks while it runs.

Child Process Recommended

The child_process module starts other programs from Node.js. exec runs a shell command and buffers the output, execFile runs a binary without a shell, spawn streams the output as it is produced, and fork starts another Node.js script with a message channel.

import { spawn } from 'node:child_process';
const p = spawn('ls', ['-la']);
p.stdout.on('data', (d) => process.stdout.write(d));
p.on('close', (code) => console.log('exit', code));

Security matters: never put user input into a shell string with exec, because it allows command injection. Pass arguments as an array to execFile or spawn. Use a child process to run existing command line tools, such as ffmpeg or git, or to isolate code that may crash. For parallel JavaScript prefer worker threads, which start faster and share memory more easily.

Practice. Run git log through spawn, stream its output, and parse the last ten commit subjects.

Cluster Recommended

One Node.js process uses one core for JavaScript. The cluster module forks several worker processes that share a server port, so a service can use all cores on a machine. The primary process listens, distributes connections to the workers, and restarts any that die.

import cluster from 'node:cluster';
import { availableParallelism } from 'node:os';
if (cluster.isPrimary) {
  for (let i = 0; i < availableParallelism(); i++) cluster.fork();
} else {
  startServer();
}

The workers do not share memory, so sessions, caches and rate limit counters must live in an external store such as Redis. Today most teams scale with containers: run one process per container and let an orchestrator such as Kubernetes Roadmap run several replicas behind a load balancer. Cluster remains useful on a single server or when PM2 manages processes.

Practice. Run a server in cluster mode, call it repeatedly, and print the process id in each response to see the spread.

Garbage Collection Recommended

V8 manages memory for you. Objects live on the heap and are freed when nothing can reach them. The collector is generational, based on the observation that most objects die young. New objects go to a small young space that is cleaned often and quickly; survivors move to the old space, which is cleaned less often with incremental and concurrent marking to keep pauses short.

What you can influence: allocate fewer short-lived objects in hot paths, avoid keeping large structures reachable longer than needed, and process big data as streams. Useful flags include --max-old-space-size to set the heap limit, and --trace-gc to print collections.

process.memoryUsage() reports heapUsed, heapTotal and rss.

Practice. Print process.memoryUsage every second while allocating arrays in a loop and note when the heap drops.

Memory Leaks Core

A leak is memory that stays reachable although the program no longer needs it, so heap use grows until the process crashes with an out-of-memory error. In Node.js the usual causes are:

  • Global caches or maps that only grow, with no size limit or expiry.
  • Event listeners added on every request and never removed.
  • Timers or intervals that keep references alive.
  • Closures that capture large objects.
  • Unbounded queues when a producer outpaces a consumer.

Finding one: watch heapUsed over time for a steady climb, then take heap snapshots with --inspect and DevTools, or v8.writeHeapSnapshot(). Compare two snapshots taken minutes apart and look for object types that multiply. Fix by bounding caches (an LRU cache or a WeakMap for object-keyed data), removing listeners, and clearing timers.

Practice. Write a deliberately leaking server, observe the heap grow, find the leak with two snapshots, and fix it.

Keep your app running in Production Core

In production a crash must not mean downtime. Keep App Running by putting a supervisor in front of the process. Pm2 is a Node.js process manager that restarts crashed processes, runs a cluster, rotates logs, reloads without downtime and can start on boot. Alternatives are systemd on Linux and, more commonly now, a container platform that restarts failed containers and replaces unhealthy ones.

pm2 start app.js -i max --name api
pm2 logs api
pm2 reload api

Good production practice: set NODE_ENV=production, run an active LTS release, handle SIGTERM with graceful shutdown, expose health and readiness endpoints, keep configuration in the environment, and put a reverse proxy such as Nginx or a cloud load balancer in front for TLS and buffering.

Practice. Run your service under PM2 with two instances, kill one process by hand and confirm it comes back.

Projects to build

Build these in order. Each one should end up in a public repository with a short README that explains your decisions.

  1. Command line todo managerBeginner

    Build a tool with add, list, complete and delete commands. Store tasks in a JSON file, parse arguments with Commander.js and return correct exit codes.

    Modules · fs/promises · Commander.js · Exit codes

  2. Log file analyserBeginner

    Read a large access log as a stream, count requests per status code and path, and print the top ten. It must handle a file larger than memory.

    Streams · pipeline · Readline · Maps and sorting

  3. Notes REST APIIntermediate

    Build an API with Express that supports create, read, update and delete for notes, with schema validation, error middleware, request logging and a database of your choice.

    Express · Validation · Error handling · Database access · Logging

  4. Authenticated API with testsIntermediate

    Add registration, login and protected routes with hashed passwords and JWT. Cover the routes with Supertest tests and run them in GitHub Actions.

    Authentication · JWT · Supertest · Vitest or Jest · Continuous integration

  5. Real-time chat serviceAdvanced

    Create a chat server with WebSockets, rooms, message history in a database and rate limiting. Share state across two instances using Redis, and ship it in a container.

    WebSockets · Event emitters · Redis · Docker · Graceful shutdown

  6. Image processing queueAdvanced

    Accept uploads through an API, queue resize jobs, process them in worker threads and expose job status. Add metrics for queue length and event loop delay.

    Worker threads · Queues · Streams · Monitoring

Interview questions

Try to answer each question out loud before you open the answer.

What is the event loop and how does it work?Junior

The event loop lets a single thread handle many operations. JavaScript runs on the call stack. When it empties, Node.js takes the next ready callback from the phase queues: timers, I/O polling, check (setImmediate) and close. Between callbacks it drains process.nextTick and then promise microtasks. Slow I/O is delegated to the operating system or the libuv thread pool, and its callback is queued on completion. A long synchronous loop blocks everything, because nothing else can run until the stack is clear.

What is the difference between process.nextTick, setImmediate and setTimeout(fn, 0)?Mid

nextTick callbacks run immediately after the current operation, before the loop moves on and before promise callbacks. setImmediate runs in the check phase, after the poll phase. setTimeout with zero runs in the timers phase after at least one millisecond. Inside an I/O callback, setImmediate always runs first. Excessive nextTick recursion can starve I/O, so I use it rarely and prefer queueMicrotask or setImmediate.

How do callbacks, promises and async/await relate?Junior

They are three ways to express the same asynchronous behaviour. A callback is a function called on completion, with an error-first convention. A promise is a placeholder object for a result that is not ready yet, and it can be chained and combined. async/await is syntax over promises that lets sequential-looking code pause at each await, with try/catch for errors. Under the hood an async function returns a promise, and await schedules the rest of the function as a microtask.

How would you run several independent async calls and handle failure?Mid

I start them together and await Promise.all, which rejects as soon as one fails. If I want every result regardless of failures, I use Promise.allSettled and inspect each status. Awaiting in a loop would run them one by one, which wastes time. For large lists I limit concurrency with a small pool, so I do not overwhelm a database or an upstream API. Each call gets a timeout through AbortSignal.timeout.

What is the difference between CommonJS and ES modules?Junior

CommonJS uses require and module.exports. It loads synchronously, and values are copied at require time. ES modules use import and export, are parsed statically, load asynchronously, support top-level await and export live bindings. Node.js decides by file extension or the type field in package.json. ESM needs explicit extensions in relative paths and has no __dirname. I choose ESM for new code and keep CommonJS only where a dependency or tool forces it.

When would you use a stream instead of reading a file with readFile?Mid

Whenever the data might be large or arrives over time. readFile loads everything into memory, so a big file can exhaust the heap and delay the first byte. A stream processes chunks as they come, with flat memory use, and lets me pipe straight to a response or a compressor. I use stream.pipeline so errors propagate and resources close. Backpressure keeps a fast reader from overwhelming a slow writer.

How does Node.js handle CPU-intensive tasks?Mid

Badly on the main thread, because JavaScript runs on one thread and a heavy computation blocks the event loop and all requests. Options are worker threads, which run JavaScript in parallel with message passing and optional shared memory, child processes for separate programs, or moving the work to a queue with dedicated workers or another service. For I/O-bound work none of this is needed, since I/O is already non-blocking.

What is the difference between worker threads, child processes and the cluster module?Senior

Worker threads share a process, can transfer or share memory and start quickly; they suit CPU-heavy JavaScript. Child processes are separate operating system processes, started to run any program or a forked script, with message passing over pipes. Cluster is a layer over fork that lets several worker processes accept connections on one port, to use all cores for a server. Cluster workers share nothing, so shared state needs an external store.

How do you handle errors in an Express application?Mid

Synchronous errors and, in Express 5, rejected promises from async handlers reach the error-handling middleware, which has four parameters and is registered last. In Express 4 I wrap async handlers to forward errors to next. I throw custom error classes carrying a status code, map them to responses in one place, log the full error with a request id, and return a generic message for unexpected errors. Process-level handlers log and exit, and a supervisor restarts the process.

What happens on an unhandled promise rejection?Mid

Since Node.js 15 the default is to raise it as an uncaught exception and terminate the process with a non-zero code. An unhandledRejection event fires first if a listener exists. The right response is to fix the missing catch. As a safety net I log the error, flush logs and exit so the supervisor restarts a clean process, because the state after an unexpected failure cannot be trusted.

How do you secure a Node.js API?Senior

I validate all input with schemas, use parameterised queries, hash passwords with argon2 or bcrypt, and keep secrets in the environment. I add helmet headers, a request size limit, rate limiting on login and expensive routes, and a strict CORS list. Authorisation is checked on every record access, not only at login. I keep dependencies updated with npm audit, run an LTS version, avoid exec with user input and return generic errors with details only in logs.

How would you find and fix a memory leak?Senior

First confirm it: heapUsed climbs over time and does not fall after garbage collection. Then I take heap snapshots with the inspector, or v8.writeHeapSnapshot, a few minutes apart and compare them to see which object types grow and what retains them. Typical causes are unbounded caches, listeners added per request, timers and closures holding large objects. I fix by bounding or expiring caches, removing listeners, clearing timers, and then re-run the same load to check the line is flat.

What is backpressure?Senior

Backpressure is the mechanism that stops a fast producer from overwhelming a slow consumer. In Node.js streams, write() returns false when the internal buffer is full, and the producer should wait for the drain event. pipeline and pipe do this automatically. Without it, data piles up in memory. The same idea applies to queues and HTTP services: bound the queue and reject or slow callers when it fills.

How do you test a Node.js service?Mid

I use layers. Unit tests for pure logic with Vitest, Jest or node:test, with time and network dependencies replaced by doubles. Integration tests that call the app in memory with Supertest against a real test database, reset between tests. A few end-to-end tests of critical journeys. Everything runs in CI on each pull request, with coverage as a guide and not a target. Tests assert on behaviour, such as status codes and stored data, not on internals.

How do you shut down a Node.js server gracefully?Senior

On SIGTERM I stop accepting connections with server.close, let in-flight requests finish within a time limit, close the database pool and other resources, and then exit with code 0. I also flip the readiness endpoint to failing first, so the load balancer stops sending traffic. If shutdown takes too long, a timer forces exit with a non-zero code. Without this, every deployment would drop some requests.

How would you choose between Express, fastify, Hono and NestJS?Senior

Express when the team knows it and the ecosystem of middleware matters. fastify when I want schema-based validation, structure and higher throughput. Hono when the code must run on edge or multiple runtimes. NestJS for a large TypeScript codebase where modules, dependency injection and conventions keep many developers aligned. For a small service the differences are minor; the fundamentals of routing, middleware and error handling carry across all of them.

Common mistakes

  • Blocking the event loop with synchronous work such as large JSON parsing or crypto in a handler; move it to a worker or break it into chunks.
  • Forgetting await, so a promise is passed around instead of its value; turn on a lint rule for floating promises.
  • Using the synchronous fs functions inside request handlers; use the promise versions.
  • Ignoring the error argument in callbacks or leaving promise chains without a catch; always handle or forward errors.
  • Reading configuration and secrets from files committed to the repository; use environment variables and a secrets manager.
  • Trusting user input in file paths, shell commands and SQL; validate it, resolve and check paths, and use parameterised queries.
  • Opening a new database connection per request; create one pool at start-up and reuse it.
  • Calling process.exit in the middle of work and losing pending writes; set the exit code and clean up first.
  • Installing packages without reading what they are, or without a lock file; commit the lock file and run npm audit.
  • Running with node app.js in production with no supervisor and no health checks; use a container platform or PM2 and add readiness endpoints.

Ready-for-interview checklist

  • I can explain the phases of the event loop and predict the order of timers, immediates, nextTick and promises.
  • I can write async code with async/await, run tasks in parallel and handle their errors.
  • I can choose between ESM and CommonJS and configure a package.json with exports and scripts.
  • I can process a large file with stream.pipeline and explain backpressure.
  • I can build an Express API with validation, middleware and a central error handler.
  • I can implement login with hashed passwords and a token or session, and protect routes.
  • I can connect to a database through a pool and explain the N+1 problem.
  • I can write unit and integration tests and run them in CI.
  • I can debug with node --inspect and read a stack trace.
  • I can take a heap snapshot and describe common causes of memory leaks.
  • I can move CPU-heavy work to a worker thread and explain when threads do not help.
  • I can containerise a service, handle SIGTERM and explain the health checks I would add.

Related roadmaps

FAQ

How long does it take to learn Node.js?

If you know JavaScript well, you can build a small API in two to three weeks. Reaching the level expected in a junior or mid-level interview, including asynchronous behaviour, testing, a database and deployment, usually takes ten to fourteen weeks at around eight hours a week. Building projects matters more than reading.

Should I learn JavaScript before Node.js?

Yes. Node.js is a runtime for JavaScript, so questions about closures, promises, classes and modules will come up. Spend a few weeks on the language with the JavaScript Roadmap roadmap, then return here. Learning TypeScript afterwards is a common next step.

Is Node.js still worth learning?

Yes. It remains widely used for APIs, tooling and server-rendered front ends, and the language skills carry over to the browser. Deno and Bun are alternative runtimes, but they follow the same language and many of the same APIs, so what you learn here transfers.

Is Node.js a framework?

No. Node.js is a runtime, an environment that executes JavaScript. Express, fastify, Hono and NestJS are frameworks that run on it. You can build a server with only the built-in http module, but frameworks save time on routing, parsing and structure.

Is Node.js single-threaded?

Your JavaScript runs on one main thread, but Node.js is not single-threaded overall. The libuv thread pool performs some file system, DNS and crypto work, and you can start worker threads or processes for parallel computation. That combination is why it handles many connections well and CPU-heavy tasks poorly.

Practise while you learn.Solve interview problems in the browser, with hints and a timer.
CodingSQLSystem design
Next Post Previous Post
No Comment
Add Comment
comment url