FREE PREVIEW

You're viewing a free preview

This is a sample of 16 questions from our full collection of 45 interview questions.

Unlock all 45 questions with detailed explanations and code examples

Get Full Access

Node.js Fundamentals

What is Node.js and how does it differ from traditional web servers like Apache or Nginx?

Node.js is a JavaScript runtime environment built on Chrome's V8 JavaScript engine that allows developers to execute JavaScript code server-side. It was created by Ryan Dahl in 2009 to enable JavaScript development outside of web browsers.

Key Characteristics of Node.js:

  • Runtime Environment: Node.js is not a web server itself, but a platform for building server applications
  • JavaScript Everywhere: Enables full-stack JavaScript development
  • Event-Driven Architecture: Built around an event-driven, non-blocking I/O model
  • Single-Threaded Event Loop: Uses one main thread with asynchronous operations

Differences from Traditional Web Servers:

Aspect Node.js Apache/Nginx
Architecture Application runtime and development platform Web server software
Threading Model Single-threaded event loop with async I/O Multi-threaded (Apache) / Event-driven (Nginx)
Primary Use Building web applications and APIs Serving static content and reverse proxying
Language JavaScript Configuration-based
Request Handling Non-blocking, asynchronous Blocking (Apache) / Non-blocking (Nginx)

Traditional web servers like Apache and Nginx are primarily designed to serve static content, handle HTTP requests, and act as reverse proxies. They require additional technologies (PHP, Python, Ruby) for dynamic content generation.

References:

↑ Back to top

What are the key differences between the CommonJS and ES Modules module systems in Node.js?

Node.js supports two module systems: CommonJS (traditional) and ES Modules (modern standard).

CommonJS (require/module.exports)

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

function multiply(a, b) {
    return a * b;
}

module.exports = { add, multiply };

// app.js - CommonJS import
const { add, multiply } = require('./math');
const result = add(5, 3);

ES Modules (import/export)

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

export function multiply(a, b) {
    return a * b;
}

// Default export
export default function subtract(a, b) {
    return a - b;
}

// app.js - ES Module import
import subtract, { add, multiply } from './math.js';
const result = add(5, 3);

Key Differences:

Feature CommonJS ES Modules
Syntax require() / module.exports import / export
Loading Synchronous Asynchronous
Timing Runtime Compile-time
Tree Shaking Not supported Supported
Top-level await Not supported Supported
File Extension .js (default) .mjs or .js with "type": "module"
Circular Dependencies Handled dynamically Static analysis

Enabling ES Modules in Node.js:

  1. Using .mjs extension:
// file.mjs
   import fs from 'fs';
  1. Using package.json:
{
     "type": "module",
     "name": "my-app"
   }

Migration Considerations:

  • Interoperability: Can import CommonJS from ES modules, but not vice versa directly
  • Dynamic Imports: Use import() for conditional loading in ES modules
  • File Extensions: Must include .js extension in ES module imports

References:

↑ Back to top

Asynchronous Programming and Event Loop

What are Promises, and how do they improve the developer experience compared to callbacks?

Promises are objects representing the eventual completion or failure of an asynchronous operation. They provide a cleaner, more manageable way to handle asynchronous code compared to traditional callbacks.

Promise States:

  • Pending: Initial state, neither fulfilled nor rejected
  • Fulfilled: Operation completed successfully
  • Rejected: Operation failed

Improvements over callbacks:

  1. Better Error Handling: Centralized error handling with .catch()
  2. Chaining: Sequential operations with .then()
  3. Readability: Flatter code structure
  4. Promise.all() and Promise.race(): Handle multiple async operations
// Callback approach (callback hell)
fs.readFile('file1.txt', (err, data1) => {
    if (err) throw err;
    fs.readFile('file2.txt', (err, data2) => {
        if (err) throw err;
        fs.readFile('file3.txt', (err, data3) => {
            if (err) throw err;
            console.log('All files read');
        });
    });
});

// Promise approach
const readFilePromise = (filename) => {
    return new Promise((resolve, reject) => {
        fs.readFile(filename, 'utf8', (err, data) => {
            if (err) reject(err);
            else resolve(data);
        });
    });
};

readFilePromise('file1.txt')
    .then(data1 => readFilePromise('file2.txt'))
    .then(data2 => readFilePromise('file3.txt'))
    .then(data3 => console.log('All files read'))
    .catch(err => console.error('Error:', err));

References:

↑ Back to top

How does async/await simplify asynchronous code? Provide an example scenario.

Async/await is syntactic sugar built on top of Promises that makes asynchronous code look and behave more like synchronous code. It significantly improves code readability and maintainability.

Key Benefits:

  • Synchronous-like syntax: Write async code that reads like sync code
  • Better error handling: Use try/catch blocks instead of .catch()
  • Easier debugging: Stack traces are more meaningful
  • Cleaner conditional logic: Avoid complex Promise chains

Example Scenario: User Registration Process

// Promise-based approach
function registerUser(userData) {
    return validateUserData(userData)
        .then(validatedData => {
            return checkUserExists(validatedData.email);
        })
        .then(userExists => {
            if (userExists) {
                throw new Error('User already exists');
            }
            return hashPassword(userData.password);
        })
        .then(hashedPassword => {
            return saveUserToDatabase({
                ...userData,
                password: hashedPassword
            });
        })
        .then(savedUser => {
            return sendWelcomeEmail(savedUser.email);
        })
        .then(() => {
            return { success: true, message: 'User registered successfully' };
        })
        .catch(error => {
            return { success: false, error: error.message };
        });
}

// Async/await approach
async function registerUser(userData) {
    try {
        const validatedData = await validateUserData(userData);
        
        const userExists = await checkUserExists(validatedData.email);
        if (userExists) {
            throw new Error('User already exists');
        }
        
        const hashedPassword = await hashPassword(userData.password);
        
        const savedUser = await saveUserToDatabase({
            ...userData,
            password: hashedPassword
        });
        
        await sendWelcomeEmail(savedUser.email);
        
        return { success: true, message: 'User registered successfully' };
    } catch (error) {
        return { success: false, error: error.message };
    }
}

// Usage
async function handleRegistration() {
    const userData = {
        email: 'user@example.com',
        password: 'secretPassword',
        name: 'John Doe'
    };
    
    const result = await registerUser(userData);
    console.log(result);
}

References:

↑ Back to top

What is the event loop and how does it manage asynchronous operations?

The event loop is the core mechanism that enables Node.js to perform non-blocking I/O operations despite being single-threaded. It's responsible for executing code, collecting and processing events, and executing queued sub-tasks.

Event Loop Phases:

graph TD
    A[Timer Phase] --> B[Pending Callbacks Phase]
    B --> C[Idle, Prepare Phase]
    C --> D[Poll Phase]
    D --> E[Check Phase]
    E --> F[Close Callbacks Phase]
    F --> A

Phase Breakdown:

  1. Timer Phase: Executes callbacks scheduled by setTimeout() and setInterval()
  2. Pending Callbacks Phase: Executes I/O callbacks deferred to the next loop iteration
  3. Idle, Prepare Phase: Internal use only
  4. Poll Phase: Fetches new I/O events; executes I/O related callbacks
  5. Check Phase: Executes setImmediate() callbacks
  6. Close Callbacks Phase: Executes close event callbacks (e.g., socket.on('close'))

How it manages async operations:

console.log('1: Start');

setTimeout(() => console.log('2: setTimeout'), 0);

setImmediate(() => console.log('3: setImmediate'));

process.nextTick(() => console.log('4: nextTick'));

Promise.resolve().then(() => console.log('5: Promise'));

console.log('6: End');

// Output order:
// 1: Start
// 6: End  
// 4: nextTick
// 5: Promise
// 3: setImmediate
// 2: setTimeout

Priority Queue:

  1. process.nextTick() (highest priority)
  2. Promise callbacks (microtasks)
  3. setImmediate()
  4. setTimeout()/setInterval()

References:

↑ Back to top

NPM and Package Management

What is npm and why is it vital for Node.js development?

npm (Node Package Manager) is the default package manager for Node.js and the world's largest software registry. It serves multiple critical functions:

Core Functions:

  • Package Installation: Downloads and installs JavaScript packages from the npm registry
  • Dependency Management: Automatically handles package dependencies and their versions
  • Script Runner: Executes custom scripts defined in package.json
  • Version Control: Manages package versions and updates
  • Publishing Platform: Allows developers to share their own packages

Why npm is Vital:

  1. Ecosystem Access: Provides access to over 1.3 million packages, enabling rapid development
  2. Dependency Resolution: Automatically resolves complex dependency trees
  3. Code Reusability: Eliminates the need to write common functionality from scratch
  4. Community Standards: Establishes consistent practices across the Node.js ecosystem
  5. Build Automation: Integrates with build tools and deployment pipelines
// Example: Installing and using a package
npm install axios

// In your code
const axios = require('axios');
const response = await axios.get('https://api.example.com/data');

References:

↑ Back to top

What is the purpose of the package.json file?

The package.json file is the heart of any Node.js project, serving as a manifest that describes the project and its dependencies.

Core Purposes:

1. Project Metadata

{
  "name": "my-awesome-project",
  "version": "1.0.0",
  "description": "A comprehensive Node.js application",
  "author": "John Doe <john@example.com>",
  "license": "MIT",
  "keywords": ["nodejs", "express", "api"],
  "homepage": "https://github.com/user/project#readme",
  "repository": {
    "type": "git",
    "url": "https://github.com/user/project.git"
  }
}

2. Dependency Management

{
  "dependencies": {
    "express": "^4.18.0",
    "mongoose": "^6.0.0"
  },
  "devDependencies": {
    "nodemon": "^2.0.0",
    "jest": "^28.0.0",
    "eslint": "^8.0.0"
  }
}

3. Script Automation

{
  "scripts": {
    "start": "node server.js",
    "dev": "nodemon server.js",
    "test": "jest",
    "build": "webpack --mode=production",
    "lint": "eslint src/",
    "deploy": "npm run build && npm run test && git push heroku main"
  }
}

4. Entry Points

{
  "main": "index.js",
  "bin": {
    "my-cli": "./bin/cli.js"
  },
  "exports": {
    ".": "./lib/index.js",
    "./utils": "./lib/utils.js"
  }
}

5. Configuration

{
  "engines": {
    "node": ">=16.0.0",
    "npm": ">=8.0.0"
  },
  "os": ["linux", "darwin"],
  "cpu": ["x64"],
  "private": true,
  "workspaces": ["packages/*"]
}

Key Benefits:

  • Reproducible Builds: Ensures consistent environments
  • Automation: Enables script execution via npm run
  • Documentation: Serves as project documentation
  • Publishing: Required for npm package publication
  • Tooling Integration: Integrates with IDEs and build tools

References:

↑ Back to top

Can you explain the difference between dependencies and devDependencies?

The distinction between dependencies and devDependencies is crucial for proper package management and application deployment:

Dependencies vs DevDependencies

Dependencies

Purpose: Packages required for the application to run in production

{
  "dependencies": {
    "express": "^4.18.0",
    "mongoose": "^6.0.0",
    "axios": "^1.0.0",
    "lodash": "^4.17.0",
    "dotenv": "^16.0.0"
  }
}

Characteristics:

  • Essential for application functionality
  • Installed in production environments
  • Included when others install your package
  • Downloaded with npm install by default

DevDependencies

Purpose: Packages needed only during development and testing

{
  "devDependencies": {
    "nodemon": "^2.0.0",
    "jest": "^28.0.0",
    "eslint": "^8.0.0",
    "prettier": "^2.7.0",
    "webpack": "^5.0.0",
    "@types/node": "^18.0.0"
  }
}

Characteristics:

  • Used for development, testing, and build processes
  • Not installed in production (with npm ci --production)
  • Not included when others install your package as a dependency
  • Installed only with npm install (not with npm install --production)

Installation Commands

# Install production dependency
npm install express --save
npm install express  # --save is default

# Install development dependency
npm install jest --save-dev
npm install jest -D  # shorthand

# Install without saving to package.json
npm install package-name --no-save

# Production installation (excludes devDependencies)
npm install --production
npm ci --production

Real-World Example

{
  "name": "e-commerce-api",
  "dependencies": {
    "express": "^4.18.0",        // Web framework - needed in production
    "mongoose": "^6.0.0",        // Database ORM - needed in production  
    "bcrypt": "^5.0.0",          // Password hashing - needed in production
    "jsonwebtoken": "^8.5.0",    // JWT tokens - needed in production
    "helmet": "^6.0.0"           // Security middleware - needed in production
  },
  "devDependencies": {
    "nodemon": "^2.0.0",         // Development server - dev only
    "jest": "^28.0.0",           // Testing framework - dev only
    "supertest": "^6.0.0",       // API testing - dev only
    "eslint": "^8.0.0",          // Code linting - dev only
    "prettier": "^2.7.0",        // Code formatting - dev only
    "@types/express": "^4.17.0"  // TypeScript types - dev only
  }
}

Other Dependency Types

PeerDependencies

{
  "peerDependencies": {
    "react": ">=16.0.0",
    "react-dom": ">=16.0.0"
  }
}
  • Required by the package but not automatically installed
  • Must be installed by the consuming application
  • Common in plugins and extensions

OptionalDependencies

{
  "optionalDependencies": {
    "fsevents": "^2.0.0"  // Mac-specific file watching
  }
}
  • Not required for core functionality
  • Installation failure doesn't break the process
  • Often platform-specific packages

Best Practices:

  1. Accurate Classification: Ensure packages are in the correct category
  2. Bundle Size Optimization: Keep dependencies minimal for production
  3. Security: Regularly audit both dependency types
  4. Documentation: Comment on unusual dependency choices
// Example: Checking if in development mode
if (process.env.NODE_ENV === 'development') {
  // Only load dev tools in development
  const debug = require('debug');
  debug('app:startup')('Starting in development mode');
}

Impact on Deployment:

# Development environment
npm install  # Installs both dependencies and devDependencies

# Production environment  
npm ci --production  # Installs only dependencies
# or
NODE_ENV=production npm ci

References:

↑ Back to top

Express.js and Web Frameworks

What are middleware functions in Express and how are they used?

Middleware functions are functions that execute during the request-response cycle in Express.js. They have access to the request object (req), response object (res), and the next middleware function.

Types of Middleware:

1. Application-level middleware

// Executed for every request
app.use((req, res, next) => {
    console.log(`${req.method} ${req.path} - ${new Date().toISOString()}`);
    next();
});

// Path-specific middleware
app.use('/api', (req, res, next) => {
    console.log('API middleware');
    next();
});

2. Router-level middleware

const router = express.Router();

router.use((req, res, next) => {
    console.log('Router middleware');
    next();
});

router.get('/users', (req, res) => {
    res.json({ users: [] });
});

app.use('/api', router);

3. Built-in middleware

app.use(express.json()); // Parse JSON bodies
app.use(express.static('public')); // Serve static files
app.use(express.urlencoded({ extended: true })); // Parse URL-encoded bodies

4. Third-party middleware

const cors = require('cors');
const morgan = require('morgan');

app.use(cors()); // Enable CORS
app.use(morgan('combined')); // HTTP request logger

5. Error-handling middleware

app.use((err, req, res, next) => {
    console.error(err.stack);
    res.status(500).json({ error: 'Something went wrong!' });
});

Middleware execution flow diagram:

Request → Middleware 1 → Middleware 2 → Route Handler → Response
    ↑         ↓            ↓              ↓
    └─────── next() ──── next() ──── next() (optional)

Key principles:

  • Always call next() unless you're ending the request-response cycle
  • Order matters - middleware executes in the order it's defined
  • Error-handling middleware must have 4 parameters

References:

↑ Back to top

What is the role of error handling middleware in an Express application?

Error handling middleware in Express serves as a centralized mechanism for catching, processing, and responding to errors that occur during request processing.

Characteristics of Error Handling Middleware:

Error handling middleware functions are defined with four parameters: (err, req, res, next)

// Basic error handling middleware
app.use((err, req, res, next) => {
    console.error(err.stack);
    res.status(500).json({ 
        error: 'Internal Server Error',
        message: process.env.NODE_ENV === 'development' ? err.message : 'Something went wrong'
    });
});

Comprehensive Error Handling Implementation:

1. Custom Error Classes

// utils/AppError.js
class AppError extends Error {
    constructor(message, statusCode) {
        super(message);
        this.statusCode = statusCode;
        this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error';
        this.isOperational = true;

        Error.captureStackTrace(this, this.constructor);
    }
}

module.exports = AppError;

2. Async Error Handler Wrapper

// utils/asyncHandler.js
const asyncHandler = (fn) => (req, res, next) => {
    Promise.resolve(fn(req, res, next)).catch(next);
};

module.exports = asyncHandler;

3. Comprehensive Error Handling Middleware

// middleware/errorHandler.js
const AppError = require('../utils/AppError');

const handleCastErrorDB = (err) => {
    const message = `Invalid ${err.path}: ${err.value}`;
    return new AppError(message, 400);
};

const handleDuplicateFieldsDB = (err) => {
    const value = err.errmsg.match(/(["'])(\\?.)*?\1/)[0];
    const message = `Duplicate field value: ${value}. Please use another value!`;
    return new AppError(message, 400);
};

const handleValidationErrorDB = (err) => {
    const errors = Object.values(err.errors).map(el => el.message);
    const message = `Invalid input data. ${errors.join('. ')}`;
    return new AppError(message, 400);
};

const handleJWTError = () =>
    new AppError('Invalid token. Please log in again!', 401);

const handleJWTExpiredError = () =>
    new AppError('Your token has expired! Please log in again.', 401);

const sendErrorDev = (err, req, res) => {
    return res.status(err.statusCode).json({
        status: err.status,
        error: err,
        message: err.message,
        stack: err.stack
    });
};

const sendErrorProd = (err, req, res) => {
    // Operational, trusted error: send message to client
    if (err.isOperational) {
        return res.status(err.statusCode).json({
            status: err.status,
            message: err.message
        });
    }
    
    // Programming or other unknown error: don't leak error details
    console.error('ERROR 💥', err);
    return res.status(500).json({
        status: 'error',
        message: 'Something went very wrong!'
    });
};

module.exports = (err, req, res, next) => {
    err.statusCode = err.statusCode || 500;
    err.status = err.status || 'error';

    if (process.env.NODE_ENV === 'development') {
        sendErrorDev(err, req, res);
    } else {
        let error = { ...err };
        error.message = err.message;

        if (error.name === 'CastError') error = handleCastErrorDB(error);
        if (error.code === 11000) error = handleDuplicateFieldsDB(error);
        if (error.name === 'ValidationError') error = handleValidationErrorDB(error);
        if (error.name === 'JsonWebTokenError') error = handleJWTError();
        if (error.name === 'TokenExpiredError') error = handleJWTExpiredError();

        sendErrorProd(error, req, res);
    }
};

4. Usage in Controllers

// controllers/userController.js
const User = require('../models/User');
const AppError = require('../utils/AppError');
const asyncHandler = require('../utils/asyncHandler');

const getUser = asyncHandler(async (req, res, next) => {
    const user = await User.findById(req.params.id);
    
    if (!user) {
        return next(new AppError('No user found with that ID', 404));
    }
    
    res.status(200).json({
        status: 'success',
        data: { user }
    });
});

module.exports = { getUser };

5. Global Unhandled Error Catching

// server.js
const app = require('./src/app');

// Handle uncaught exceptions
process.on('uncaughtException', (err) => {
    console.log('💥 UNCAUGHT EXCEPTION! Shutting down...');
    console.log(err.name, err.message);
    process.exit(1);
});

const server = app.listen(PORT, () => {
    console.log(`Server running on port ${PORT}`);
});

// Handle unhandled promise rejections
process.on('unhandledRejection', (err) => {
    console.log('💥 UNHANDLED REJECTION! Shutting down...');
    console.log(err.name, err.message);
    server.close(() => {
        process.exit(1);
    });
});

Error Handling Flow Diagram:

Request → Middleware → Controller → Service
    ↓         ↓           ↓          ↓
Error occurs → Error caught → next(err) → Error Middleware
    ↓
Error processed → Response sent → End

Key Benefits:

  1. Centralized Error Processing: All errors flow through a single point
  2. Consistent Error Responses: Uniform error format across the application
  3. Environment-Specific Handling: Different error details for development vs production
  4. Operational vs Programming Errors: Distinction between expected and unexpected errors
  5. Error Logging: Centralized logging for monitoring and debugging

References:

↑ Back to top

Databases and Data Management

How do you connect a Node.js application to a database (e.g., MongoDB, PostgreSQL)?

Connecting a Node.js application to databases involves using appropriate database drivers or client libraries. The approach varies depending on the database type:

MongoDB Connection

For MongoDB, you can use the native MongoDB driver or Mongoose (ODM):

Using Native MongoDB Driver:

const { MongoClient } = require('mongodb');

const uri = 'mongodb://localhost:27017';
const client = new MongoClient(uri);

async function connectToMongoDB() {
  try {
    await client.connect();
    console.log('Connected to MongoDB');
    const database = client.db('myapp');
    return database;
  } catch (error) {
    console.error('MongoDB connection error:', error);
  }
}

Using Mongoose:

const mongoose = require('mongoose');

async function connectToMongoDB() {
  try {
    await mongoose.connect('mongodb://localhost:27017/myapp', {
      useNewUrlParser: true,
      useUnifiedTopology: true
    });
    console.log('Connected to MongoDB via Mongoose');
  } catch (error) {
    console.error('Mongoose connection error:', error);
  }
}

PostgreSQL Connection

For PostgreSQL, use the pg library:

Using pg (node-postgres):

const { Pool } = require('pg');

const pool = new Pool({
  user: 'username',
  host: 'localhost',
  database: 'myapp',
  password: 'password',
  port: 5432,
  max: 20,
  idleTimeoutMillis: 30000,
});

async function connectToPostgreSQL() {
  try {
    const client = await pool.connect();
    console.log('Connected to PostgreSQL');
    return client;
  } catch (error) {
    console.error('PostgreSQL connection error:', error);
  }
}

Best practices for database connections:

  • Use connection pooling to manage multiple database connections efficiently
  • Store database credentials in environment variables, not in code
  • Implement proper error handling and connection retry logic
  • Close connections properly to prevent memory leaks

References:

↑ Back to top

Testing and Debugging

What tools and libraries are available for testing Node.js applications?

Node.js has a rich ecosystem of testing tools and libraries that cater to different testing needs:

Testing Frameworks

  • Jest - The most popular JavaScript testing framework [1], providing an all-in-one solution with built-in test runner, assertion library, mocking capabilities, and code coverage
  • Mocha - A mature, feature-rich testing framework [2] that provides the test structure but requires additional libraries for assertions
  • Vitest - A modern, fast testing framework [1] that offers Jest-compatible API with better performance and native ESM support
  • Node.js Test Runner - The built-in test runner introduced in Node.js 18+ [2]

Assertion Libraries

  • Chai - A BDD/TDD assertion library that pairs well with Mocha
  • Jest Expect - Built into Jest, providing expressive assertion methods
  • Node.js Assert - The built-in assertion module

HTTP/API Testing

  • Supertest - Essential for testing HTTP endpoints and Express.js applications [3]
  • Nock - HTTP server mocking library for testing external API calls

End-to-End Testing

  • Cypress - Modern E2E testing framework [4]
  • Playwright - Cross-browser automation framework [5]
  • Puppeteer - Chrome DevTools Protocol-based testing

Mocking and Stubbing

  • Sinon.js - Standalone library for spies, stubs, and mocks
  • Jest Mocks - Built-in mocking capabilities in Jest
↑ Back to top

Performance Optimization and Scaling

What is clustering in Node.js and how does it improve performance?

Clustering in Node.js is a built-in module that allows you to create child processes (workers) that share server ports, enabling your application to take advantage of multi-core systems.

How Clustering Works:

const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;

if (cluster.isMaster) {
  console.log(`Master ${process.pid} is running`);
  
  // Fork workers
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }
  
  cluster.on('exit', (worker, code, signal) => {
    console.log(`Worker ${worker.process.pid} died`);
    cluster.fork(); // Restart worker
  });
} else {
  // Workers can share any TCP port
  http.createServer((req, res) => {
    res.writeHead(200);
    res.end('Hello from worker ' + process.pid);
  }).listen(8000);
  
  console.log(`Worker ${process.pid} started`);
}

Performance Improvements:

  1. CPU Utilization: Leverages all available CPU cores instead of just one
  2. Load Distribution: Requests are automatically distributed among worker processes
  3. Fault Tolerance: If one worker crashes, others continue serving requests
  4. Increased Throughput: Can handle more concurrent requests by running multiple instances

Benefits:

  • Zero Downtime Deployments: Workers can be restarted individually
  • Automatic Load Balancing: Built-in round-robin load balancing
  • Process Isolation: Crashes in one worker don't affect others

Considerations:

  • Memory Usage: Each worker consumes additional memory
  • Shared State: Workers don't share memory, requiring external solutions for shared data
  • Not Suitable for: CPU-intensive tasks that would block all workers

References:

↑ Back to top

Security Best Practices

What are some common security vulnerabilities in Node.js applications?

Node.js applications face several critical security vulnerabilities that developers must address:

Injection Attacks

  • SQL Injection: Occurs when untrusted data is sent to SQL queries without proper sanitization
  • NoSQL Injection: Similar to SQL injection but targets NoSQL databases like MongoDB
  • Command Injection: Happens when user input is passed to system commands without validation

Cross-Site Scripting (XSS)

  • Stored XSS: Malicious scripts stored in the database and executed when viewed
  • Reflected XSS: Scripts reflected off web servers in error messages or search results
  • DOM-based XSS: Client-side scripts that modify the DOM in unsafe ways

Authentication and Session Vulnerabilities

  • Broken Authentication: Weak password policies, session fixation, or improper logout
  • Session Hijacking: Unauthorized access to user sessions through various attack vectors
  • JWT Vulnerabilities: Weak signing keys, algorithm confusion, or improper token validation

Dependency Vulnerabilities

  • Outdated Dependencies: Using packages with known security flaws
  • Supply Chain Attacks: Malicious code introduced through compromised dependencies
  • Prototype Pollution: Modifying Object.prototype affecting the entire application

Server-Side Request Forgery (SSRF)

  • Applications making requests to internal resources based on user input
  • Can lead to internal network scanning or accessing sensitive endpoints

Insecure Direct Object References

  • Exposing internal implementation objects without proper authorization checks
  • Allowing users to access resources they shouldn't have permission to view

References:

↑ Back to top

How do you manage authentication and authorization in a Node.js application?

Authentication Strategies

JWT (JSON Web Tokens)

const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');

// Login endpoint
app.post('/login', async (req, res) => {
  const { username, password } = req.body;
  
  // Validate user credentials
  const user = await User.findOne({ username });
  if (!user || !await bcrypt.compare(password, user.password)) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }
  
  // Generate JWT
  const token = jwt.sign(
    { userId: user._id, role: user.role },
    process.env.JWT_SECRET,
    { expiresIn: '1h' }
  );
  
  res.json({ token });
});

// JWT verification middleware
const authenticateToken = (req, res, next) => {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1];
  
  if (!token) {
    return res.status(401).json({ error: 'Access token required' });
  }
  
  jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
    if (err) return res.status(403).json({ error: 'Invalid token' });
    req.user = user;
    next();
  });
};

Session-Based Authentication

const session = require('express-session');
const MongoStore = require('connect-mongo');

app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  store: MongoStore.create({
    mongoUrl: process.env.MONGODB_URI
  }),
  cookie: {
    secure: process.env.NODE_ENV === 'production',
    httpOnly: true,
    maxAge: 24 * 60 * 60 * 1000 // 24 hours
  }
}));

// Session authentication middleware
const requireAuth = (req, res, next) => {
  if (!req.session.userId) {
    return res.status(401).json({ error: 'Authentication required' });
  }
  next();
};

Authorization (Role-Based Access Control)

// Role-based authorization middleware
const authorize = (roles) => {
  return (req, res, next) => {
    if (!req.user) {
      return res.status(401).json({ error: 'Authentication required' });
    }
    
    if (!roles.includes(req.user.role)) {
      return res.status(403).json({ error: 'Insufficient privileges' });
    }
    
    next();
  };
};

// Usage examples
app.get('/admin', authenticateToken, authorize(['admin']), (req, res) => {
  res.json({ message: 'Admin only content' });
});

app.get('/user-data', authenticateToken, authorize(['user', 'admin']), (req, res) => {
  res.json({ message: 'User data' });
});

Multi-Factor Authentication (MFA)

const speakeasy = require('speakeasy');
const QRCode = require('qrcode');

// Generate MFA secret
const generateMFASecret = async (userId) => {
  const secret = speakeasy.generateSecret({
    name: `YourApp (${userId})`,
    issuer: 'YourApp'
  });
  
  // Save secret to database
  await User.findByIdAndUpdate(userId, { mfaSecret: secret.base32 });
  
  // Generate QR code
  const qrCodeUrl = await QRCode.toDataURL(secret.otpauth_url);
  return { secret: secret.base32, qrCode: qrCodeUrl };
};

// Verify MFA token
const verifyMFA = (req, res, next) => {
  const { token } = req.body;
  const user = req.user;
  
  const verified = speakeasy.totp.verify({
    secret: user.mfaSecret,
    encoding: 'base32',
    token: token,
    window: 2
  });
  
  if (!verified) {
    return res.status(401).json({ error: 'Invalid MFA token' });
  }
  
  next();
};

References:

↑ Back to top

Advanced Topics and Specialized Knowledge Areas

What are streams in Node.js and how can they be utilized for efficient data processing?

Streams are Node.js objects that let you read data from a source or write data to a destination in a continuous fashion. They are particularly powerful for handling large amounts of data efficiently without loading everything into memory.

Types of Streams:

1. Readable Streams

  • Read data from a source (files, HTTP requests, stdin)
  • Examples: fs.createReadStream(), process.stdin

2. Writable Streams

  • Write data to a destination (files, HTTP responses, stdout)
  • Examples: fs.createWriteStream(), process.stdout

3. Duplex Streams

  • Both readable and writable
  • Examples: TCP sockets, crypto streams

4. Transform Streams

  • Modify data as it passes through
  • Examples: zlib compression, CSV parsers

Efficient Data Processing Use Cases:

const fs = require('fs');
const zlib = require('zlib');
const csv = require('csv-parser');

// File processing pipeline
fs.createReadStream('large-file.csv')
  .pipe(csv())
  .pipe(new Transform({
    objectMode: true,
    transform(chunk, encoding, callback) {
      // Process each row
      const processed = {
        ...chunk,
        processedAt: new Date().toISOString()
      };
      callback(null, JSON.stringify(processed) + '\n');
    }
  }))
  .pipe(zlib.createGzip())
  .pipe(fs.createWriteStream('processed-data.json.gz'));

Memory Efficiency Benefits:

  • Backpressure: Automatic flow control prevents memory overflow
  • Chunked Processing: Process data piece by piece
  • Pipeline Composition: Chain multiple processing steps efficiently

Performance Diagram:

Traditional Approach:
[Load Entire File] → [Process in Memory] → [Write Result]
Memory Usage: ████████████████████████████████

Stream Approach:
[Read Chunk] → [Process Chunk] → [Write Chunk] → [Next Chunk]
Memory Usage: ████

References:

↑ Back to top

Want more questions?

You've seen 16 sample questions. Unlock all 45 En interview questions with detailed explanations, code examples, and expert insights.

45+ questions
Code examples
Expert explanations
Instant access
Unlock Full Access