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.
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.
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.
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}/`); });
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); });
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.
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'); });
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}`);
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'); });
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.
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:
myModule.js
:// myModule.js function greet(name) { return `Hello, ${name}!`; } module.exports = greet;
app.js
:// app.js const greet = require('./myModule'); console.log(greet('World')); // Output: Hello, World!
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:
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'); });
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.
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'); });
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.
To connect an application to MongoDB using Mongoose, you need to follow these steps:
connect
method.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); });
Performance optimization in Node.js can be achieved through several techniques:
process.memoryUsage()
to track memory consumption.compression
in Express.js to reduce the size of the response body and improve load times.profiler
or third-party tools like New Relic to monitor performance and identify bottlenecks.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'); });
The package.json
file is a central repository of configuration for tools and libraries in a Node.js project. It includes several key fields:
npm run
command.Securing a Node.js application against common vulnerabilities involves implementing several best practices and strategies:
validator
and express-validator
to ensure inputs are safe.pg
for PostgreSQL or mysql2
for MySQL support this feature.helmet
to set HTTP headers that help protect against XSS.csurf
can be integrated into your application to generate and validate CSRF tokens.passport
can help manage authentication strategies.npm audit
to identify and fix security issues in your dependencies.express-rate-limit
can help manage request rates.dotenv
to manage environment variables.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:
require
and module.exports
, while ES6 modules use import
and export
.