Interview

20 Node.js Interview Questions and Answers

Prepare for your next technical interview with this guide on Node.js, featuring common and advanced questions to enhance your understanding and skills.

Node.js has revolutionized server-side development with its non-blocking, event-driven architecture. Known for its efficiency and scalability, Node.js is widely adopted for building fast, real-time applications. Its single-threaded model, combined with the power of JavaScript, allows developers to create high-performance network applications with ease.

This article offers a curated selection of interview questions designed to test your knowledge and proficiency in Node.js. By working through these questions, you will gain a deeper understanding of key concepts and be better prepared to demonstrate your expertise in any technical interview setting.

Node.js Interview Questions and Answers

1. Explain how the event loop works and the role of callbacks.

The event loop in Node.js manages asynchronous operations, allowing non-blocking I/O operations despite JavaScript’s single-threaded nature. It checks the call stack and callback queue, executing callbacks when the stack is empty.

Callbacks are functions passed as arguments to other functions, executed after an asynchronous operation completes. They enable non-blocking execution of asynchronous code.

Example:

const fs = require('fs');

console.log('Start');

fs.readFile('example.txt', 'utf8', (err, data) => {
    if (err) throw err;
    console.log(data);
});

console.log('End');

In this example, fs.readFile is asynchronous. The callback is placed in the queue and executed by the event loop once the file reading is complete and the stack is empty.

2. Write code to create a simple HTTP server that responds with “Hello World”.

To create a simple HTTP server in Node.js that responds with “Hello World,” use the built-in http module. This module provides functions to create an HTTP server and handle requests and responses.

const http = require('http');

const server = http.createServer((req, res) => {
    res.statusCode = 200;
    res.setHeader('Content-Type', 'text/plain');
    res.end('Hello World\n');
});

const port = 3000;
server.listen(port, () => {
    console.log(`Server running at http://localhost:${port}/`);
});

3. Demonstrate how to handle asynchronous operations using Promises.

Promises in Node.js handle asynchronous operations, offering a more manageable way to work with asynchronous code compared to callbacks. A Promise represents an operation that hasn’t completed yet but is expected to in the future. It can be pending, fulfilled, or rejected.

Example:

function asyncOperation() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            const success = true;
            if (success) {
                resolve('Operation completed successfully');
            } else {
                reject('Operation failed');
            }
        }, 1000);
    });
}

asyncOperation()
    .then(result => {
        console.log(result);
    })
    .catch(error => {
        console.error(error);
    });

In this example, asyncOperation returns a Promise that resolves after a 1-second delay. The then method handles successful resolution, while catch handles errors.

Promises can be chained for sequential asynchronous operations:

function firstOperation() {
    return new Promise((resolve) => {
        setTimeout(() => resolve('First operation completed'), 1000);
    });
}

function secondOperation() {
    return new Promise((resolve) => {
        setTimeout(() => resolve('Second operation completed'), 1000);
    });
}

firstOperation()
    .then(result => {
        console.log(result);
        return secondOperation();
    })
    .then(result => {
        console.log(result);
    })
    .catch(error => {
        console.error(error);
    });

4. Explain the purpose of middleware in Express.js and provide an example.

Middleware in Express.js consists of functions executed in the order they are defined. Each middleware function has access to the request object, the response object, and the next middleware function in the request-response cycle. Middleware can be used for logging, authentication, and parsing request bodies.

Example:

const express = require('express');
const app = express();

const requestLogger = (req, res, next) => {
    console.log(`${req.method} ${req.url}`);
    next();
};

app.use(requestLogger);

app.get('/', (req, res) => {
    res.send('Hello, world!');
});

app.listen(3000, () => {
    console.log('Server is running on port 3000');
});

In this example, requestLogger logs the HTTP method and URL of each request. The next() function passes control to the next middleware function. The app.use(requestLogger) line applies this middleware to all requests.

5. Implement error handling in an Express.js route handler.

Error handling in Express.js ensures that errors during route handler execution are properly managed, improving user experience and debugging. Error-handling middleware functions have four arguments: (err, req, res, next).

Example:

const express = require('express');
const app = express();

app.get('/example', (req, res, next) => {
    try {
        throw new Error('Something went wrong!');
    } catch (err) {
        next(err);
    }
});

app.use((err, req, res, next) => {
    console.error(err.stack);
    res.status(500).send('Internal Server Error');
});

app.listen(3000, () => {
    console.log('Server is running on port 3000');
});

6. Describe how to manage environment variables in an application.

Environment variables store configuration settings that can change between environments, such as development, testing, and production. They help keep sensitive information like API keys and database credentials out of the source code.

In Node.js, environment variables can be managed using the dotenv package, which loads variables from a .env file into process.env.

Example:

1. Install the dotenv package:

npm install dotenv

2. Create a .env file in the root directory of your project:

DB_HOST=localhost
DB_USER=root
DB_PASS=s1mpl3

3. Load the environment variables in your application:

require('dotenv').config();

const dbHost = process.env.DB_HOST;
const dbUser = process.env.DB_USER;
const dbPass = process.env.DB_PASS;

console.log(`Connecting to database at ${dbHost} with user ${dbUser}`);

7. Write code to implement JWT authentication in an Express.js application.

JWT (JSON Web Token) is a compact, URL-safe means of representing claims to be transferred between two parties. It is commonly used for authentication in web applications. In an Express.js application, JWT can secure routes by ensuring that only authenticated users can access certain endpoints.

Example:

const express = require('express');
const jwt = require('jsonwebtoken');
const bodyParser = require('body-parser');

const app = express();
app.use(bodyParser.json());

const secretKey = 'your_secret_key';

const authenticateJWT = (req, res, next) => {
    const token = req.header('Authorization');
    if (token) {
        jwt.verify(token, secretKey, (err, user) => {
            if (err) {
                return res.sendStatus(403);
            }
            req.user = user;
            next();
        });
    } else {
        res.sendStatus(401);
    }
};

app.post('/login', (req, res) => {
    const { username, password } = req.body;
    const user = { username };
    const token = jwt.sign(user, secretKey, { expiresIn: '1h' });
    res.json({ token });
});

app.get('/protected', authenticateJWT, (req, res) => {
    res.send('This is a protected route');
});

app.listen(3000, () => {
    console.log('Server started on port 3000');
});

8. Explain how the cluster module can be used to scale an application.

The cluster module in Node.js allows you to create child processes (workers) that run simultaneously and share the same server port. This is useful for taking advantage of multi-core systems, as Node.js runs on a single thread by default. By distributing the load across multiple workers, you can improve the performance and reliability of your application.

Example:

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

if (cluster.isMaster) {
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }

  cluster.on('exit', (worker, code, signal) => {
    console.log(`Worker ${worker.process.pid} died`);
  });
} else {
  http.createServer((req, res) => {
    res.writeHead(200);
    res.end('Hello, world!\n');
  }).listen(8000);
}

In this example, the master process forks a worker for each CPU core available. Each worker runs an HTTP server that listens on the same port (8000). If a worker dies, the master process can log the event and potentially fork a new worker to replace it.

9. Write code to create and use a custom module.

In Node.js, a custom module is a file that contains code which can be reused across different parts of an application. Custom modules help in organizing code and promoting reusability. To create a custom module, you define the module in a separate file and export the necessary functions or variables. Then, you can import and use this module in other files using the require function.

Example:

  • Create a custom module in a file named myModule.js:
// myModule.js
function greet(name) {
    return `Hello, ${name}!`;
}

module.exports = greet;
  • Use the custom module in another file named app.js:
// app.js
const greet = require('./myModule');

console.log(greet('World')); // Output: Hello, World!

10. Explain the security best practices that should be followed.

Security best practices in Node.js are important to ensure that applications are protected from vulnerabilities and attacks. Here are some key practices to follow:

  • Dependency Management: Regularly update dependencies and use tools like npm audit to identify and fix vulnerabilities in third-party packages.
  • Data Validation and Sanitization: Always validate and sanitize user inputs to prevent injection attacks such as SQL injection and cross-site scripting (XSS).
  • Environment Variables: Store sensitive information such as API keys and database credentials in environment variables, not in the source code.
  • Use HTTPS: Ensure that data transmitted between the client and server is encrypted by using HTTPS.
  • Rate Limiting: Implement rate limiting to protect against brute force attacks and denial-of-service (DoS) attacks.
  • Error Handling: Avoid exposing stack traces and detailed error messages to the end user. Use generic error messages and log detailed errors on the server side.
  • Secure Coding Practices: Follow secure coding practices such as avoiding the use of eval(), using prepared statements for database queries, and employing security linters.
  • Content Security Policy (CSP): Implement CSP headers to mitigate the risk of XSS attacks by specifying which sources are allowed to load content on your site.
  • Authentication and Authorization: Use strong authentication mechanisms and ensure proper authorization checks are in place to protect sensitive routes and resources.

11. Write code to implement a WebSocket server and client.

WebSockets provide a full-duplex communication channel over a single TCP connection, allowing for real-time data transfer between a client and a server. In Node.js, the ws library is commonly used to implement WebSocket servers and clients.

Example of a WebSocket server:

const WebSocket = require('ws');

const server = new WebSocket.Server({ port: 8080 });

server.on('connection', (ws) => {
    console.log('Client connected');
    
    ws.on('message', (message) => {
        console.log(`Received: ${message}`);
        ws.send(`Echo: ${message}`);
    });

    ws.on('close', () => {
        console.log('Client disconnected');
    });
});

console.log('WebSocket server is running on ws://localhost:8080');

Example of a WebSocket client:

const WebSocket = require('ws');

const client = new WebSocket('ws://localhost:8080');

client.on('open', () => {
    console.log('Connected to server');
    client.send('Hello Server!');
});

client.on('message', (message) => {
    console.log(`Received: ${message}`);
});

client.on('close', () => {
    console.log('Disconnected from server');
});

12. Describe how to use npm scripts to automate tasks.

npm scripts are a feature of npm (Node Package Manager) that allow you to define and run custom scripts to automate various tasks in your Node.js projects. These tasks can include running tests, building your project, linting code, and more. npm scripts are defined in the “scripts” section of the package.json file.

Example:

{
  "name": "my-project",
  "version": "1.0.0",
  "scripts": {
    "start": "node app.js",
    "test": "mocha",
    "build": "webpack --config webpack.config.js",
    "lint": "eslint ."
  }
}

In this example, the “scripts” section defines four tasks: “start”, “test”, “build”, and “lint”. Each task is associated with a command that will be executed when the script is run.

To run an npm script, you use the npm run command followed by the name of the script. For example:

npm run start
npm run test
npm run build
npm run lint

npm also provides lifecycle scripts, such as “prestart” and “poststart”, which run before and after the “start” script, respectively. This allows for more complex automation workflows.

13. Write code to handle CORS in an Express.js application.

CORS (Cross-Origin Resource Sharing) is a security feature implemented by web browsers to prevent malicious websites from making requests to a different domain than the one that served the web page. In an Express.js application, handling CORS is essential when your frontend and backend are hosted on different domains or ports.

To handle CORS in an Express.js application, you can use the cors middleware. This middleware allows you to specify which domains are permitted to access your resources.

Example:

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

const app = express();

// Use the CORS middleware
app.use(cors({
    origin: 'http://example.com', // Replace with your frontend domain
    methods: ['GET', 'POST', 'PUT', 'DELETE'],
    allowedHeaders: ['Content-Type', 'Authorization']
}));

app.get('/', (req, res) => {
    res.send('CORS is enabled for this route');
});

app.listen(3000, () => {
    console.log('Server is running on port 3000');
});

14. Demonstrate how to write unit tests using Mocha and Chai.

Mocha is a feature-rich JavaScript test framework running on Node.js, making asynchronous testing simple and fun. Chai is a BDD/TDD assertion library for Node.js that can be paired with any JavaScript testing framework. Together, they provide a powerful combination for writing unit tests.

Example:

// Importing Mocha and Chai
const { expect } = require('chai');
const { describe, it } = require('mocha');

// Function to be tested
function add(a, b) {
    return a + b;
}

// Unit tests
describe('Addition Function', () => {
    it('should return 5 when adding 2 and 3', () => {
        expect(add(2, 3)).to.equal(5);
    });

    it('should return 0 when adding -1 and 1', () => {
        expect(add(-1, 1)).to.equal(0);
    });
});

In this example, we define a simple add function and write unit tests to verify its correctness. The describe block groups related tests, and the it block defines individual test cases. The expect function from Chai is used to make assertions about the expected outcomes.

15. Write code to connect an application to MongoDB using Mongoose.

To connect an application to MongoDB using Mongoose, you need to follow these steps:

  • Import Mongoose.
  • Connect to the MongoDB database using the connect method.
  • Handle connection events to ensure the connection is successful or to catch any errors.

Here is a concise example:

const mongoose = require('mongoose');

const uri = 'mongodb://localhost:27017/mydatabase';

mongoose.connect(uri, { useNewUrlParser: true, useUnifiedTopology: true })
  .then(() => {
    console.log('Successfully connected to MongoDB');
  })
  .catch(err => {
    console.error('Connection error', err);
  });

16. Describe various performance optimization techniques.

Performance optimization in Node.js can be achieved through several techniques:

  • Asynchronous Programming: Node.js is designed to handle asynchronous operations efficiently. Using asynchronous methods (callbacks, promises, async/await) helps in non-blocking I/O operations, which improves performance.
  • Efficient Use of Middleware: In frameworks like Express.js, middleware functions can add overhead. Ensure that middleware is only used when necessary and avoid redundant middleware.
  • Clustering: Node.js runs on a single thread by default. Clustering allows you to take advantage of multi-core systems by creating child processes that share the same server port.
  • Caching: Implement caching strategies to store frequently accessed data in memory. This reduces the need to repeatedly fetch data from databases or external APIs.
  • Load Balancing: Distribute incoming requests across multiple servers to ensure no single server becomes a bottleneck.
  • Memory Management: Monitor and manage memory usage to prevent memory leaks. Use tools like Node.js built-in process.memoryUsage() to track memory consumption.
  • Error Handling: Proper error handling ensures that the application does not crash unexpectedly. Use try-catch blocks and handle promise rejections to manage errors gracefully.
  • Database Optimization: Optimize database queries and use indexing to speed up data retrieval. Consider using connection pooling to manage database connections efficiently.
  • Compression: Use compression middleware like compression in Express.js to reduce the size of the response body and improve load times.
  • Profiling and Monitoring: Use profiling tools like Node.js built-in profiler or third-party tools like New Relic to monitor performance and identify bottlenecks.

17. Write code to implement rate limiting in an Express.js API.

Rate limiting is a technique used to control the number of requests a client can make to a server within a specified time frame. This is crucial for preventing abuse, ensuring fair usage, and protecting the server from being overwhelmed by too many requests. In an Express.js API, rate limiting can be implemented using middleware such as express-rate-limit.

Example:

const express = require('express');
const rateLimit = require('express-rate-limit');

const app = express();

// Define the rate limit rule
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // limit each IP to 100 requests per windowMs
  message: 'Too many requests from this IP, please try again after 15 minutes'
});

// Apply the rate limit rule to all requests
app.use(limiter);

app.get('/', (req, res) => {
  res.send('Hello, world!');
});

app.listen(3000, () => {
  console.log('Server is running on port 3000');
});

18. Explain the role of package.json and its key fields.

The package.json file is a central repository of configuration for tools and libraries in a Node.js project. It includes several key fields:

  • name: Specifies the name of the project.
  • version: Indicates the current version of the project, following semantic versioning.
  • description: Provides a brief description of the project.
  • main: Specifies the entry point of the application, typically the main JavaScript file.
  • scripts: Defines a set of node scripts that can be run using npm run command.
  • dependencies: Lists the packages required by the project to run in production.
  • devDependencies: Lists the packages required only for development and testing.
  • repository: Contains information about the source code repository.
  • keywords: An array of strings that helps identify the project in search results.
  • author: Specifies the author of the project.
  • license: Indicates the license under which the project is distributed.

19. Explain how to secure an application against common vulnerabilities.

Securing a Node.js application against common vulnerabilities involves implementing several best practices and strategies:

  • Input Validation and Sanitization: Always validate and sanitize user inputs to prevent SQL injection and other injection attacks. Use libraries like validator and express-validator to ensure inputs are safe.
  • Use Prepared Statements: When interacting with databases, use prepared statements or parameterized queries to prevent SQL injection attacks. Libraries like pg for PostgreSQL or mysql2 for MySQL support this feature.
  • Escape Output: To prevent Cross-Site Scripting (XSS) attacks, ensure that any data rendered in the browser is properly escaped. Use libraries like helmet to set HTTP headers that help protect against XSS.
  • Implement CSRF Protection: Use CSRF tokens to protect against Cross-Site Request Forgery (CSRF) attacks. Libraries like csurf can be integrated into your application to generate and validate CSRF tokens.
  • Secure Authentication and Authorization: Use strong authentication mechanisms, such as OAuth or JWT, and ensure that sensitive routes are protected with proper authorization checks. Libraries like passport can help manage authentication strategies.
  • Use HTTPS: Always use HTTPS to encrypt data transmitted between the client and server. This helps protect against man-in-the-middle attacks.
  • Keep Dependencies Updated: Regularly update your dependencies to ensure that you are protected against known vulnerabilities. Use tools like npm audit to identify and fix security issues in your dependencies.
  • Limit Rate of Requests: Implement rate limiting to protect against brute force attacks and denial-of-service (DoS) attacks. Libraries like express-rate-limit can help manage request rates.
  • Environment Variables: Store sensitive information, such as API keys and database credentials, in environment variables rather than hardcoding them in your source code. Use libraries like dotenv to manage environment variables.
  • Error Handling: Avoid exposing detailed error messages to users, as they can reveal sensitive information about your application. Use generic error messages and log detailed errors on the server side.

20. Discuss the differences between CommonJS and ES6 modules.

CommonJS and ES6 modules are two different module systems used in JavaScript to manage dependencies and organize code.

CommonJS is the module system used by Node.js. It uses the require function to import modules and module.exports to export them. This system is synchronous, meaning modules are loaded at runtime.

Example of CommonJS:

// Exporting a module
module.exports = {
    sayHello: function() {
        console.log("Hello, world!");
    }
};

// Importing a module
const myModule = require('./myModule');
myModule.sayHello();

ES6 modules, on the other hand, are part of the ECMAScript 2015 (ES6) standard and are used in modern JavaScript. They use the import and export keywords and are designed to be statically analyzable, meaning the dependencies are known at compile time. This allows for better optimization and tree-shaking.

Example of ES6 modules:

// Exporting a module
export function sayHello() {
    console.log("Hello, world!");
}

// Importing a module
import { sayHello } from './myModule';
sayHello();

Key differences include:

  • Syntax: CommonJS uses require and module.exports, while ES6 modules use import and export.
  • Loading: CommonJS is synchronous and loads modules at runtime, whereas ES6 modules are asynchronous and can be statically analyzed.
  • Scope: CommonJS modules are wrapped in a function, giving them their own scope. ES6 modules have their own scope by default.
  • Usage: CommonJS is primarily used in Node.js, while ES6 modules are used in modern JavaScript environments, including browsers.
Previous

15 Modern C++ Interview Questions and Answers

Back to Interview
Next

15 Amazon Frontend Interview Questions and Answers