Node.js

Node.js Fundamentals

15 Questions

Node.js is a JavaScript runtime built on Chrome's V8 engine that allows running JavaScript on the server side. It uses an event-driven, non-blocking I/O model that makes it lightweight and efficient for building scalable network applications. Unlike browser JavaScript, Node.js has no DOM or window object but provides access to the file system, network, and OS-level APIs. It ships with built-in modules like fs, http, path, and crypto for server-side development.
// Node.js provides server-side APIs
const fs = require('fs');
const http = require('http');

http.createServer((req, res) => {
  res.end('Hello from Node.js');
}).listen(3000);

Why it matters: This tests your core understanding of what Node.js is as a runtime. Interviewers use this to assess whether you grasp the server-side nature of Node.js and why it differs fundamentally from browser-based JavaScript in terms of available APIs and execution context.

Real applications: Companies like Netflix, LinkedIn, and Uber use Node.js to power their backend APIs, handling millions of concurrent requests efficiently without spawning thousands of OS threads.

Common mistakes: Developers assume Node.js is just "JavaScript on the server" without understanding the lack of DOM/BOM APIs, or they try to use browser-specific globals like localStorage or document inside Node.js causing immediate runtime errors.

require() is the CommonJS module system built into Node.js, while import is the ES Module syntax added in Node.js 12 with full support from version 14. The two systems differ fundamentally — require() is synchronous and can be called conditionally anywhere, while import is statically analyzed at parse time and supports tree-shaking. ES Modules use strict mode by default, support top-level await, and produce live bindings rather than copied values. Mixing the two requires careful configuration with "type": "module" in package.json or using .mjs/.cjs file extensions.
// CommonJS
const express = require('express');
const { join } = require('path');

// ES Modules (requires "type": "module" in package.json)
import express from 'express';
import { join } from 'path';

Why it matters: Understanding module system differences is critical for migrating legacy codebases, troubleshooting ERR_REQUIRE_ESM errors, and making architectural decisions about new projects. Senior roles demand clarity on when to use each system.

Real applications: Legacy Express apps use require() everywhere, while newer projects using TypeScript or Vite use import/export. Many teams migrate gradually, making coexistence knowledge essential.

Common mistakes: Mixing CJS and ESM without proper configuration causes cryptic errors. Developers also forget that import must be at the top level — you cannot use it inside a function or conditional block like require().

package.json is the manifest file that holds metadata, dependencies, and scripts for a Node.js project. It defines the project's name, version, entry point, and lists both production and development dependencies. The scripts section allows defining custom commands executable with npm run, and the engines field can specify required Node.js versions. It is the single source of truth for project configuration and is essential for reproducible builds and collaboration.
{
  "name": "my-app",
  "version": "1.0.0",
  "scripts": { "start": "node index.js", "test": "jest" },
  "dependencies": { "express": "^4.18.2" },
  "devDependencies": { "jest": "^29.0.0" }
}

Why it matters: Every Node.js project relies on package.json, and interviewers want to know if you understand dependency management, semantic versioning, and project configuration beyond just running npm install.

Real applications: CI/CD pipelines read package.json scripts to run tests, build steps, and lint checks. DevOps teams use the version field for deployment tagging and rollback strategies across staging and production environments.

Common mistakes: Developers commit node_modules to version control instead of relying on package.json, or they manually edit version numbers instead of using npm version patch/minor/major which also creates git tags automatically.

In Node.js the global object is global, equivalent to window in browsers, holding globally accessible variables and functions. Since Node.js 12, globalThis provides a universal reference that works consistently across both Node.js and browser environments for isomorphic code. Common global utilities like console, setTimeout, Buffer, and process are available without requiring any module. Variables declared with const, let, or var at the top of a module are scoped to that module — not added to global.
console.log(global === globalThis); // true

global.myVar = 'accessible everywhere';
console.log(global.myVar); // 'accessible everywhere'

// globalThis works in both Node.js and browsers
globalThis.sharedFlag = true;

Why it matters: Understanding the global scope helps avoid accidental variable pollution and explains why utilities like console and setTimeout are available everywhere without explicit imports — a common source of confusion for browser developers.

Real applications: Testing frameworks like Jest attach mock utilities to globalThis for convenience. Libraries like dotenv attach configuration globally so it's accessible throughout the entire application.

Common mistakes: Developers from browser backgrounds write window.something instead of globalThis, or they expect top-level variable declarations to be globally accessible when they are actually module-scoped.

process.nextTick() fires before any I/O events in the current iteration, while setImmediate() fires in the check phase of the next event loop iteration. The nextTick queue is processed after the current operation completes but before the event loop continues, giving it the highest priority among all async callbacks. This distinction is critical for understanding callback scheduling order in Node.js applications. Use process.nextTick() when you need code to run before I/O, and setImmediate() for work that should yield to I/O first.
setImmediate(() => console.log('setImmediate'));
process.nextTick(() => console.log('nextTick'));
Promise.resolve().then(() => console.log('promise'));
// Output:
// nextTick
// promise
// setImmediate

Why it matters: This is a classic Node.js interview question testing event loop knowledge. Getting this wrong signals shallow understanding of asynchronous execution order, which is fundamental to debugging Node.js timing issues.

Real applications: Node.js EventEmitter internally uses process.nextTick() to guarantee that event listeners are attached before events fire. setImmediate() is used in recursive async patterns to prevent starving I/O operations.

Common mistakes: Developers use process.nextTick() recursively, unknowingly starving I/O callbacks and creating an unresponsive server. The fix is to use setImmediate() for recursive patterns instead.

Environment variables are key-value pairs set outside the application that configure its behavior at runtime without modifying code. They are accessed via the process.env object, which contains all environment variables available to the current process. This approach keeps sensitive configuration like API keys and database credentials out of source code, following the twelve-factor app methodology. The dotenv package is the standard way to load variables from a .env file during local development.
// Set: PORT=3000 node app.js
const port = process.env.PORT || 3000;
const dbUrl = process.env.DATABASE_URL;

// With dotenv
require('dotenv').config();
console.log(process.env.API_KEY);

Why it matters: Secure configuration management is a key interview and production topic. Knowing how to handle secrets properly demonstrates awareness of security best practices and prevents credential leaks in application code.

Real applications: Production deployments on AWS, Heroku, and Docker pass database URLs, API keys, and feature flags via environment variables, keeping secrets out of the codebase entirely and enabling different configs per environment.

Common mistakes: Hardcoding credentials directly in source code, forgetting to add .env to .gitignore, or not providing fallback defaults for missing variables — causing unpredictable crashes when the app is deployed to a new environment.

V8 is Google's open-source JavaScript engine written in C++ that compiles JavaScript directly to native machine code for fast execution, powering both Chrome and Node.js. V8's architecture includes an interpreter called Ignition and an optimizing compiler called TurboFan that work together using JIT compilation to maximize runtime performance. It also handles garbage collection through a generational mark-and-sweep algorithm, and supports modern ECMAScript features natively. Node.js wraps V8 with libuv to add asynchronous I/O capabilities essential for server-side development.
// V8 optimizes hot code paths automatically
// This loop will be JIT-compiled to machine code after several runs
function sum(arr) {
  let total = 0;
  for (let i = 0; i < arr.length; i++) total += arr[i];
  return total;
}

// Check V8 version
console.log(process.versions.v8); // e.g., '11.8.172.17'

Why it matters: V8 knowledge shows you understand the underlying JavaScript execution model. It's relevant for performance tuning discussions and helps explain why certain patterns (like avoiding hidden class changes) are significantly faster than others.

Real applications: V8's JIT compiler means frequently called "hot" functions get optimized to machine code automatically, which is why production Node.js API servers can handle thousands of requests per second with minimal CPU overhead.

Common mistakes: Writing code that causes V8 deoptimization — like changing object property types after creation, using the arguments object in hot functions, or creating deeply polymorphic call sites — significantly reduces runtime performance.

Node.js uses a single-threaded event loop with non-blocking I/O to handle concurrency efficiently without creating a thread per connection. Heavy I/O operations are offloaded to the operating system kernel or a libuv thread pool, keeping the main JavaScript thread free to process other requests. This architecture lets Node.js handle thousands of concurrent connections with far less memory than traditional thread-per-request models. CPU-intensive tasks remain the exception — they block the loop and must be offloaded to Worker Threads or child processes.
const fs = require('fs');

// Non-blocking — callback fires when done, main thread stays free
fs.readFile('data.txt', 'utf8', (err, data) => {
  console.log(data);
});
console.log('This runs BEFORE readFile completes');

Why it matters: This question filters candidates who truly understand Node.js's architecture. It's fundamental to knowing which types of workloads Node.js excels at (I/O-bound) and where it struggles (CPU-bound), guiding proper system design.

Real applications: A Node.js API server handling 10,000 concurrent database queries keeps one JS thread free while libuv manages the actual I/O — impossible at the same memory cost with traditional thread-per-request architectures like Java Servlets.

Common mistakes: Performing CPU-intensive operations like image processing, PDF generation, or cryptographic hashing directly in request handlers blocks the event loop, making all other pending requests freeze until the operation completes.

The process object is a global EventEmitter that provides information and control over the current Node.js process without requiring any import. It exposes properties like process.pid, process.argv, process.env, and process.version for accessing runtime details. It also emits critical events like uncaughtException, unhandledRejection, and SIGTERM that are essential for graceful shutdown logic and global error handling in production apps.
console.log(process.pid);       // Process ID
console.log(process.argv);      // Command-line arguments
console.log(process.cwd());     // Current working directory
console.log(process.version);   // Node.js version

process.on('SIGTERM', () => {
  console.log('Graceful shutdown initiated...');
  server.close(() => process.exit(0));
});

Why it matters: The process object is essential for production Node.js applications. Interviewers test this to see if you know how to handle graceful shutdowns, read configuration from the environment, and manage the full application lifecycle.

Real applications: Kubernetes and Docker send SIGTERM when stopping containers. Production servers listen to this signal to stop accepting new requests, drain active connections, close DB connections, and exit cleanly — preventing data corruption.

Common mistakes: Not handling unhandledRejection and uncaughtException events, which causes silent crashes in production. Also calling process.exit(0) immediately without waiting for pending async operations like database writes to complete.

Deno is a modern JavaScript/TypeScript runtime created by Node.js's original author Ryan Dahl to address Node's design shortcomings around security, modules, and tooling. Both runtimes use the V8 engine but differ significantly — Deno is secure by default (requires explicit permission flags), has built-in TypeScript support, uses URL-based ES Modules instead of node_modules, and ships with a reviewed standard library. Node.js has a vastly larger ecosystem with npm, making it far more prevalent in production. Deno has recently added Node.js compatibility layers to ease migration and adoption.
// Deno: explicit permissions required
// deno run --allow-read --allow-net server.ts

// Deno uses URL-based imports (no npm)
import { serve } from "https://deno.land/std/http/server.ts";

// Node.js uses npm packages
const express = require('express');

Why it matters: This shows awareness of the JavaScript runtime landscape and demonstrates you keep up with ecosystem evolution. While Deno is less common in production today, it influences architectural discussions and is referenced in modern job postings.

Real applications: Some organizations choose Deno Deploy for new serverless functions or internal tools where its built-in TypeScript support and security model reduce setup overhead and eliminate configuration boilerplate.

Common mistakes: Assuming Deno is "the next Node.js" without understanding the maturity gap and smaller ecosystem. Also confusing Deno's URL-based imports with npm modules — standard npm packages do not work natively in Deno without a compatibility shim.

REPL stands for Read-Eval-Print Loop, an interactive shell built into Node.js for quick experimentation and debugging without creating files. It reads user input, evaluates it as JavaScript, prints the result, and loops back for more input — launch it by typing node in any terminal. The REPL supports tab completion, multi-line expressions, and special dot commands like .load, .save, .break, and .exit. It is particularly useful for testing regular expressions, exploring unfamiliar module APIs, and prototyping logic quickly.
// In terminal, type 'node' to start REPL
> 2 + 3
5
> const greet = name => `Hello, ${name}`;
undefined
> greet('Node')
'Hello, Node'
> .help    // shows all available REPL commands
> .exit    // exits the REPL

Why it matters: Knowledge of the REPL shows you're comfortable with Node.js's interactive environment. It's a powerful tool for quick prototyping and debugging, and knowing its capabilities distinguishes experienced developers from beginners.

Real applications: Developers use the Node.js REPL to test regular expressions, explore API responses, verify module behavior, and debug production issues interactively during incident post-mortems without deploying code changes.

Common mistakes: Not knowing how to handle multi-line expressions in the REPL (you need to press Enter after an opening brace), or being unaware of the .break command to cancel an incomplete expression that's blocking input.

__dirname returns the absolute path of the directory containing the currently executing script, while __filename returns the absolute path including the file name itself. These are module-scoped variables automatically injected by the CommonJS module wrapper — they are not available in ES Modules by default. They are essential for constructing reliable file paths relative to the current module location regardless of where the application is started from.
console.log(__dirname);   // /home/user/project/src
console.log(__filename);  // /home/user/project/src/app.js

// Reliable path resolution using __dirname
const path = require('path');
const configPath = path.join(__dirname, '..', 'config', 'db.json');

// ES Module equivalent
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __dirname = dirname(fileURLToPath(import.meta.url));

Why it matters: File path handling is essential for building robust Node.js applications that work across different operating systems and deployment environments. Incorrect paths are a common source of "file not found" bugs in production.

Real applications: Express servers use __dirname to serve static files reliably: app.use(express.static(path.join(__dirname, 'public'))), ensuring paths work correctly regardless of the working directory when the server is started.

Common mistakes: Using string concatenation instead of path.join() for building paths, which breaks on Windows due to backslash/forward-slash differences. Also assuming __dirname works in ES Modules where you must use import.meta.url instead.

Node.js provides several timer functions for scheduling code execution: setTimeout for one-time delayed execution, setInterval for repeated execution, and setImmediate for immediate post-I/O execution. These timers are not part of the V8 engine but are implemented by the Node.js runtime via libuv, and each is processed in a specific phase of the event loop. Timer callbacks are not guaranteed to fire at the exact specified time — they fire as soon as possible after the delay has elapsed and the event loop reaches the timers phase.
const timeout = setTimeout(() => console.log('once after 1s'), 1000);
const interval = setInterval(() => console.log('every 2s'), 2000);

// Cancel timers when done
clearTimeout(timeout);
clearInterval(interval);

// Executes in the check phase after I/O
setImmediate(() => console.log('after current I/O'));

// Prevent timer from keeping process alive
timeout.unref();

Why it matters: Timer behavior directly affects application correctness, especially in testing and rate-limiting logic. Interviewers check whether you understand that timers are not precise and interact with the event loop in specific ways.

Real applications: Rate limiters, debounce functions, polling systems (checking message queues), session timeouts, and heartbeat checks in WebSocket servers all rely on Node.js timer APIs in production systems.

Common mistakes: Using setInterval for critical timing without compensating for timer drift, or forgetting to call clearInterval in cleanup code — causing memory leaks when timers are created inside loops or request handlers.

The Buffer class in Node.js handles raw binary data directly in memory outside the V8 heap, representing fixed-size chunks of memory similar to arrays of bytes. Buffers are essential when working with streams, file I/O, network protocols, cryptographic operations, and any scenario involving binary data. Buffer.alloc() is the safest creation method as it zero-fills memory, while Buffer.allocUnsafe() is faster but may contain sensitive old data from previous allocations. Buffers integrate seamlessly with Node.js streams for efficient processing of large files.
const buf1 = Buffer.from('Hello Node.js');        // from string
const buf2 = Buffer.alloc(10);                    // 10 zero-filled bytes
const buf3 = Buffer.allocUnsafe(10);              // faster but uninitialized

console.log(buf1.toString());                      // 'Hello Node.js'
console.log(buf1.toString('hex'));                 // hexadecimal
console.log(buf1.length);                         // 13
console.log(Buffer.isBuffer(buf1));               // true

Why it matters: Working with binary data is fundamental for file uploads, network protocol implementations, cryptography, and image processing. Knowing Buffer is essential for systems-level Node.js work and is often tested in backend-focused interviews.

Real applications: File upload handlers use Buffers for incoming binary data, crypto libraries use them for encryption operations, and network proxy servers use them for efficient protocol parsing and manipulation.

Common mistakes: Using Buffer.allocUnsafe() without filling the buffer, which can inadvertently leak sensitive data from previous memory allocations. Also not specifying encoding when converting Buffers to strings, resulting in garbled output.

Command-line arguments in Node.js are accessed through the process.argv array — the first element is the Node.js executable path, the second is the script path, and subsequent elements are user-provided arguments. For simple cases, you can slice and parse process.argv manually, but for complex CLIs, libraries like yargs or commander provide structured parsing with validation and help text generation. Node.js 18 introduced util.parseArgs() as a built-in alternative that handles common flag patterns without external dependencies.
// Run: node app.js --name John --port 3000
const args = process.argv.slice(2);
console.log(args); // ['--name', 'John', '--port', '3000']

// Node.js 18+ built-in parseArgs
const { parseArgs } = require('node:util');
const { values } = parseArgs({
  options: {
    name: { type: 'string' },
    port: { type: 'string' }
  }
});
console.log(values.name); // 'John'

Why it matters: CLI tool development is a prevalent use case for Node.js. This question tests practical knowledge of building command-line utilities and scripts that interact with the shell environment, common in DevOps and tooling roles.

Real applications: Build tools, migration scripts, code generators, and dev utilities are commonly built as Node.js CLI apps. Tools like create-react-app, eslint, and webpack are all Node.js CLIs using argument parsing.

Common mistakes: Accessing process.argv[2] without bounds checking causing undefined, not validating argument types before use, or forgetting that the first two argv entries are always the node executable and script path — not user arguments.