Node.js Interview Questions to Ace Your Interviews

author image Hirely
at 08 Jan, 2025

Question: What is Express.js and how does it relate to Node.js?

Answer:

Express.js is a minimalist and flexible web application framework for Node.js that provides a robust set of features for building web and mobile applications. It simplifies the process of handling HTTP requests, routing, middleware integration, and templating, making it a popular choice for developers building server-side applications with Node.js.

How Express.js Relates to Node.js:

Node.js is a runtime environment that allows developers to run JavaScript on the server side. While Node.js provides basic HTTP server functionality through its core modules (such as http), it doesn’t come with a rich set of features needed for creating web applications, such as handling URL routing, middleware, sessions, and templating. This is where Express.js comes in.

Express.js is built on top of Node.js and provides a higher-level abstraction for handling web requests. It simplifies common tasks that would require extensive coding using just the core Node.js modules, allowing developers to focus on application logic rather than the intricacies of managing HTTP requests and responses.

Key Features of Express.js:

  1. Routing: Express simplifies routing, which allows you to define how different HTTP requests (e.g., GET, POST, PUT, DELETE) should be handled for specific URL paths.
    • Example:
      app.get('/home', (req, res) => {
        res.send('Welcome to the home page');
      });
  2. Middleware: Express allows you to define middleware functions that are executed during the request-response cycle. Middleware can handle tasks such as logging, authentication, request body parsing, or serving static files.
    • Example:
      app.use(express.json()); // Middleware to parse JSON request bodies
  3. Template Engine Support: Express can easily integrate with various template engines (like Pug, EJS, and Handlebars) to render dynamic HTML content.
  4. Error Handling: Express provides built-in methods for error handling, making it easier to handle both synchronous and asynchronous errors in your application.
  5. Routing Parameters: Express allows you to handle dynamic URL parameters, making it easy to build RESTful APIs.
    • Example:
      app.get('/user/:id', (req, res) => {
        const userId = req.params.id;
        res.send(`User ID: ${userId}`);
      });
  6. Support for RESTful APIs: Express is commonly used for building RESTful APIs by allowing easy routing and handling of various HTTP methods (GET, POST, PUT, DELETE).
  7. Session and Cookies: Express provides tools for managing sessions and cookies, which are often needed for web applications that involve user authentication.
  8. Static File Serving: Express can serve static files like images, stylesheets, and JavaScript files with just a few lines of code.
    • Example:
      app.use(express.static('public'));

Basic Example of an Express.js Application:

Here’s a simple example to demonstrate how Express.js works in a Node.js application:

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

// Middleware to parse incoming JSON requests
app.use(express.json());

// Route for the home page
app.get('/', (req, res) => {
  res.send('Hello, Express.js!');
});

// Route with a dynamic parameter
app.get('/user/:name', (req, res) => {
  const name = req.params.name;
  res.send(`Hello, ${name}!`);
});

// Start the server
app.listen(port, () => {
  console.log(`Server is running at http://localhost:${port}`);
});

How Express.js Enhances Node.js:

  1. Faster Development: Express abstracts the complexities of handling HTTP requests and responses, making it easier and faster to develop web applications and APIs.
  2. Cleaner Code: Express provides a clear, declarative structure for managing routes, middleware, and request handling. This reduces the boilerplate code that would be necessary if you were to use only Node.js.
  3. Middleware and Extensibility: Express has a rich ecosystem of middleware that can easily be integrated for various functionalities, such as security, logging, request validation, authentication, and more. This extensibility makes it ideal for building feature-rich applications.

How Express.js Works with Node.js:

  • Node.js provides the underlying platform that runs JavaScript on the server, handles I/O operations, and manages HTTP requests.
  • Express.js builds on top of Node.js to simplify handling HTTP requests, organizing the application into routes, managing middleware, and providing tools for templating, static file serving, and more.
  • Express.js uses the Node.js http module under the hood, but it abstracts much of the complexity to provide a more developer-friendly API.

Why Use Express.js with Node.js?

  • Rapid Development: Express simplifies the creation of web applications and APIs by providing a clean and concise API, reducing the need for manual configuration and boilerplate code.
  • Large Ecosystem: Express benefits from a large ecosystem of community-driven middleware that can be easily plugged into your application to handle a wide range of tasks (e.g., user authentication, database integration, input validation).
  • Cross-Platform: Express runs on Node.js, which is cross-platform and allows applications to be deployed on various operating systems, including Linux, macOS, and Windows.
  • Flexible: Express is minimal and flexible, meaning you can extend and modify it according to your needs. It can be used for everything from simple web servers to complex RESTful APIs.

Conclusion:

  • Express.js is a powerful, lightweight framework for building web applications and APIs with Node.js.
  • It is built on top of Node.js and simplifies common server-side tasks like routing, middleware handling, and serving static files.
  • Express is ideal for developers who want to leverage the non-blocking, event-driven nature of Node.js while minimizing the complexity of building web applications or RESTful APIs.

By using Express.js, you can accelerate development, create maintainable applications, and easily scale your Node.js applications to meet your needs.

Question: How do you handle asynchronous I/O in Node.js?

Answer:

Asynchronous I/O operations are one of the key features of Node.js, allowing it to handle multiple requests concurrently without blocking the execution of other tasks. Node.js is designed around a non-blocking, event-driven architecture, making it ideal for I/O-heavy applications, such as web servers, databases, and file systems, where tasks like reading files or handling network requests might take time but should not block the rest of the program from executing.

Here’s how asynchronous I/O is handled in Node.js:

1. Event Loop and Non-blocking I/O

At the core of asynchronous I/O in Node.js is the event loop, which continuously checks for and executes tasks that are ready to be performed. The event loop helps manage asynchronous operations by delegating tasks to the system (e.g., reading files, database queries) and then handling their results when they are complete.

When a non-blocking I/O operation is initiated, Node.js doesn’t wait for it to finish. Instead, it proceeds with other tasks, and when the I/O operation completes, it triggers a callback function or resolves a promise to handle the result.

Example of Asynchronous I/O with Callback:

The fs module (file system) in Node.js provides an asynchronous method, fs.readFile(), which reads a file without blocking the event loop.

const fs = require('fs');

// Asynchronous read of a file
fs.readFile('example.txt', 'utf8', (err, data) => {
  if (err) {
    console.error('Error reading file:', err);
    return;
  }
  console.log('File content:', data);
});

console.log('This will log first because the readFile is non-blocking.');

In this example:

  • fs.readFile() is asynchronous and initiates a non-blocking I/O operation.
  • While the file is being read, the event loop can continue processing other tasks, such as logging "This will log first because the readFile is non-blocking.".
  • Once the file is read, the provided callback function is executed with the file contents.

2. Callbacks

Callbacks are the most traditional way of handling asynchronous operations in Node.js. They are functions that are passed as arguments to asynchronous methods and are executed when the operation completes.

While callbacks work well, they can lead to a problem known as callback hell (or pyramid of doom) when multiple nested asynchronous operations need to be executed in sequence.

Example of Callback Hell:

fs.readFile('file1.txt', 'utf8', (err, data1) => {
  if (err) throw err;
  fs.readFile('file2.txt', 'utf8', (err, data2) => {
    if (err) throw err;
    fs.readFile('file3.txt', 'utf8', (err, data3) => {
      if (err) throw err;
      console.log(data1, data2, data3);
    });
  });
});

In this example, the nested callbacks can become difficult to manage and read as the complexity of the asynchronous flow increases.

3. Promises

To address the drawbacks of callbacks, Promises were introduced as a more elegant solution for handling asynchronous operations. A Promise represents the eventual completion (or failure) of an asynchronous operation and its resulting value.

Promises allow chaining and better error handling, making asynchronous code easier to read and maintain.

Example using Promises:

const fs = require('fs').promises;

fs.readFile('example.txt', 'utf8')
  .then((data) => {
    console.log('File content:', data);
  })
  .catch((err) => {
    console.error('Error reading file:', err);
  });

In this example:

  • fs.readFile() returns a Promise, which resolves with the data when the file is read successfully or rejects with an error.
  • The .then() method is used to handle the successful result, and .catch() is used to handle any errors.

4. Async/Await

Async/await is a syntactic sugar built on top of Promises and provides a way to write asynchronous code that looks and behaves like synchronous code. async functions automatically return Promises, and await allows you to pause execution until the Promise is resolved.

This makes asynchronous code much more readable, and you avoid the nested structure of callbacks or promise chains.

Example using Async/Await:

const fs = require('fs').promises;

async function readFile() {
  try {
    const data = await fs.readFile('example.txt', 'utf8');
    console.log('File content:', data);
  } catch (err) {
    console.error('Error reading file:', err);
  }
}

readFile();

In this example:

  • async ensures that the function returns a Promise.
  • await pauses the execution of the function until the Promise returned by fs.readFile() is resolved.
  • The try-catch block is used for error handling, making it more manageable than handling errors via .catch() in Promises.

5. Event Emitters

Node.js provides the EventEmitter class for handling events asynchronously. Many Node.js core modules (like HTTP and file system) use event emitters to notify when an I/O operation has completed.

Example using EventEmitter:

const EventEmitter = require('events');
const fs = require('fs');

class FileReader extends EventEmitter {
  readFile(filePath) {
    fs.readFile(filePath, 'utf8', (err, data) => {
      if (err) {
        this.emit('error', err);
      } else {
        this.emit('data', data);
      }
    });
  }
}

const reader = new FileReader();

reader.on('data', (data) => {
  console.log('File content:', data);
});

reader.on('error', (err) => {
  console.error('Error reading file:', err);
});

reader.readFile('example.txt');

In this example:

  • The FileReader class extends EventEmitter and emits data and error events based on the result of the file reading.
  • The on() method is used to listen for these events asynchronously.

6. Streams

Streams are a more efficient way of handling large I/O operations asynchronously. Instead of loading the entire file or data into memory, streams read or write data piece by piece, making them ideal for handling large files, network requests, or data from databases.

Example of Read Stream:

const fs = require('fs');

const readStream = fs.createReadStream('largefile.txt', 'utf8');

readStream.on('data', (chunk) => {
  console.log('Received chunk:', chunk);
});

readStream.on('end', () => {
  console.log('File read complete');
});

Conclusion:

Handling asynchronous I/O in Node.js is key to its performance and scalability. The most common techniques for managing asynchronous operations in Node.js are:

  • Callbacks: Handle asynchronous results by passing a callback function.
  • Promises: A more modern and readable way of handling async operations, offering chaining and better error handling.
  • Async/Await: Syntactic sugar built on promises to make async code look synchronous.
  • Event Emitters: Useful for handling events triggered by asynchronous actions, particularly for complex workflows.
  • Streams: Efficiently handle large I/O operations asynchronously.

By using these tools, you can build highly performant, non-blocking applications in Node.js that can handle thousands of simultaneous connections without being bogged down by waiting for I/O operations.

Read More

If you can’t get enough from this article, Aihirely has plenty more related information, such as Node.js interview questions, Node.js interview experiences, and details about various Node.js job positions. Click here to check it out.

Related Posts

Trace Job opportunities

Hirely, your exclusive interview companion, empowers your competence and facilitates your interviews.

Get Started Now