Nodejs Best Practices And Interview Preparation Complete Guide

 Last Update:2025-06-22T00:00:00     .NET School AI Teacher - SELECT ANY TEXT TO EXPLANATION.    11 mins read      Difficulty-Level: beginner

Online Code run

🔔 Note: Select your programming language to check or run code at

💻 Run Code Compiler

Step-by-Step Guide: How to Implement NodeJS Best Practices and Interview Preparation

1. Modular Code Structure

Problem:

Creating a scalable project structure in Node.js is essential for maintainability. A beginner often faces a challenge in organizing code properly.

Solution:

Let's create a simple application that includes controllers, models, routes, and services.

Steps:

Step 1: Create the Project Structure:

/project
  /app
    /controllers
      userController.js
    /models
      userModel.js
    /routes
      userRoutes.js
    /services
      userService.js
  server.js
  package.json

Step 2: Create the User Model (userModel.js):

const mongoose = require('mongoose');

const userSchema = new mongoose.Schema({
  name: { type: String, required: true },
  email: { type: String, required: true, unique: true },
  password: { type: String, required: true }
});

module.exports = mongoose.model('User', userSchema);

Step 3: Create the User Service (userService.js):

const User = require('../models/userModel');

const createUser = async (userData) => {
  try {
    const user = new User(userData);
    await user.save();
    return user;
  } catch (error) {
    throw error;
  }
};

const findUserByEmail = async (email) => {
  try {
    const user = await User.findOne({ email });
    return user;
  } catch (error) {
    throw error;
  }
};

module.exports = {
  createUser,
  findUserByEmail
};

Step 4: Create the User Controller (userController.js):

const userService = require('../services/userService');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');

const register = async (req, res) => {
  try {
    const { name, email, password } = req.body;
    const hashedPassword = await bcrypt.hash(password, 10);
    const user = await userService.createUser({ name, email, password: hashedPassword });

    res.status(201).json({ message: 'User created successfully', user });
  } catch (error) {
    res.status(400).json({ message: error.message });
  }
};

const login = async (req, res) => {
  try {
    const { email, password } = req.body;

    const user = await userService.findUserByEmail(email);
    if (!user) return res.status(404).json({ message: 'User not found' });

    const isMatch = await bcrypt.compare(password, user.password);
    if (!isMatch) return res.status(401).json({ message: 'Invalid credentials' });

    const token = jwt.sign({ id: user._id }, process.env.JWT_SECRET, { expiresIn: '1h' });
    res.status(200).json({ message: 'Logged in successfully', token });
  } catch (error) {
    res.status(400).json({ message: error.message });
  }
};

module.exports = {
  register,
  login
};

Step 5: Create the User Routes (userRoutes.js):

const express = require('express');
const router = express.Router();
const userController = require('../controllers/userController');

router.post('/register', userController.register);
router.post('/login', userController.login);

module.exports = router;

Step 6: Set up the Server (server.js):

require('dotenv').config();
const express = require('express');
const mongoose = require('mongoose');
const app = express();

// Middleware
app.use(express.json());

// Routes
const userRoutes = require('./app/routes/userRoutes');
app.use('/api/v1/users', userRoutes);

// Connect to MongoDB
mongoose.connect(process.env.MONGO_URI, { useNewUrlParser: true, useUnifiedTopology: true })
  .then(() => console.log('Connected to MongoDB'))
  .catch(err => console.log('Error connecting to MongoDB:', err));

// Server listen
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

2. Error Handling Using Custom Middleware

Problem:

Proper error handling is critical for maintaining robust applications. Beginners may struggle with implementing a global error handler.

Solution:

Create a custom middleware to handle errors globally.

Steps:

Step 1: Define an Error Middleware in server.js:

const errorHandlerMiddleware = (err, req, res, next) => {
  const statusCode = res.statusCode ? res.statusCode : 500;
  res.status(statusCode).json({
    message: err.message,
    stack: process.env.NODE_ENV === 'production' ? null : err.stack
  });
};

app.use(errorHandlerMiddleware);

Step 2: Use next() in Controllers to Pass Errors to Middleware:

Modify the login controller method to pass errors using next():

const login = async (req, res, next) => {
  try {
    const { email, password } = req.body;

    const user = await userService.findUserByEmail(email);
    if (!user) return res.status(404).json({ message: 'User not found' });

    const isMatch = await bcrypt.compare(password, user.password);
    if (!isMatch) return res.status(401).json({ message: 'Invalid credentials' });

    const token = jwt.sign({ id: user._id }, process.env.JWT_SECRET, { expiresIn: '1h' });
    res.status(200).json({ message: 'Logged in successfully', token });
  } catch (error) {
    next(error); // Pass the error to the error handler middleware
  }
};

3. Using Environment Variables with .env file

Problem:

Hardcoding sensitive information like API keys or database URIs in your source code is risky.

Solution:

Use environment variables to store sensitive data.

Steps:

Step 1: Install dotenv:

npm install dotenv

Step 2: Create a .env File:

In the root directory of your project, create a .env file:

DB_URI=mongodb://localhost:27017/yourdbname
JWT_SECRET=your-jwt-secret
PORT=3000

Step 3: Update Your Application to Load .env Variables:

Ensure require('dotenv').config(); is at the top of your server.js:

require('dotenv').config();
const express = require('express');
const mongoose = require('mongoose');
const app = express();

// Middleware
app.use(express.json());

// Routes
const userRoutes = require('./app/routes/userRoutes');
app.use('/api/v1/users', userRoutes);

// Connect to MongoDB
mongoose.connect(process.env.DB_URI, { useNewUrlParser: true, useUnifiedTopology: true })
  .then(() => console.log('Connected to MongoDB'))
  .catch(err => console.log('Error connecting to MongoDB:', err));

// Server listen
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

// Error Middleware
const errorHandlerMiddleware = (err, req, res, next) => {
  const statusCode = res.statusCode ? res.statusCode : 500;
  res.status(statusCode).json({
    message: err.message,
    stack: process.env.NODE_ENV === 'production' ? null : err.stack
  });
};

app.use(errorHandlerMiddleware);

4. Basic Security Practices

Problem:

Security is often overlooked in beginner projects.

Solution:

Implement basic security measures in your Node.js app.

Steps:

Step 1: Install Required Packages:

npm install helmet cors express-rate-limit

Step 2: Configure Security Middleware in server.js:

require('dotenv').config();
const express = require('express');
const mongoose = require('mongoose');
const helmet = require('helmet');
const cors = require('cors');
const rateLimit = require('express-rate-limit');

const app = express();

// Security Middleware
app.use(helmet());
app.use(cors());

// Rate Limiting
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100 // limit each IP to 100 requests per windowMs
});

app.use(limiter);

// Middleware
app.use(express.json());

// Routes
const userRoutes = require('./app/routes/userRoutes');
app.use('/api/v1/users', userRoutes);

// Connect to MongoDB
mongoose.connect(process.env.DB_URI, { useNewUrlParser: true, useUnifiedTopology: true })
  .then(() => console.log('Connected to MongoDB'))
  .catch(err => console.log('Error connecting to MongoDB:', err));

// Server listen
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

// Error Middleware
const errorHandlerMiddleware = (err, req, res, next) => {
  const statusCode = res.statusCode ? res.statusCode : 500;
  res.status(statusCode).json({
    message: err.message,
    stack: process.env.NODE_ENV === 'production' ? null : err.stack
  });
};

app.use(errorHandlerMiddleware);

5. Basic Testing with Mocha and Chai

Problem:

Testing is an important part of software development to ensure your code works as expected.

Solution:

Create unit tests for your user model using Mocha and Chai.

Steps:

Step 1: Install Mocha and Chai:

npm install --save-dev mocha chai chai-http

Step 2: Set Up Test Structure:

Add a test folder and structure:

/project
  /test
    /models
      userTest.js

Step 3: Write a Test Case for the User Model (userTest.js):

const chai = require('chai');
const expect = chai.expect;
const chaiHttp = require('chai-http');
const server = require('../../server');
const mongoose = require('mongoose');
const User = require('../../app/models/userModel');

chai.use(chaiHttp);

describe('User Tests', function() {
  after(function(done) {
    mongoose.connection.close(done);
  });

  describe('POST /api/v1/users/register', function() {
    it('should register a new user', function(done) {
      const userData = {
        name: 'John Doe',
        email: 'johndoe@test.com',
        password: 'password123'
      };

      chai.request(server)
        .post('/api/v1/users/register')
        .send(userData)
        .end(async function(err, res) {
          expect(res).to.have.status(201);
          expect(res.body).to.be.an('object');
          expect(res.body.user).to.include.keys('_id', 'name', 'email', 'createdAt', '__v');
          done();
        });
    });

    it('should not register a user if email already exists', async function() {
      const existingUserData = {
        name: 'Jane Doe',
        email: 'janedoe@test.com',
        password: 'password123'
      };

      // Create a user first
      await User.create(existingUserData);

      const duplicateUserData = {
        name: 'Jane Doe',
        email: 'janedoe@test.com',
        password: 'diffpassword'
      };

      const res = await chai.request(server)
        .post('/api/v1/users/register')
        .send(duplicateUserData);

      expect(res).to.have.status(400);
    });
  });

  describe('POST /api/v1/users/login', function() {
    it('should log in an existing user', async function() {
      const userData = {
        name: 'Jane Doe',
        email: 'janedoe@test.com',
        password: 'password123'
      };

      // Create a user first for this test case
      await User.create(userData);

      const res = await chai.request(server)
        .post('/api/v1/users/login')
        .send({ email: 'janedoe@test.com', password: 'password123' });

      expect(res).to.have.status(200);
      expect(res.body).to.be.an('object');
      expect(res.body.token).to.be.a('string');
    });

    it('should not log in a non-existing user', async function() {
      const res = await chai.request(server)
        .post('/api/v1/users/login')
        .send({ email: 'nonexistent@test.com', password: 'password123' });

      expect(res).to.have.status(404);
    });
  });
});

Step 4: Update package.json to Include a Test Script:

Ensure your package.json includes a test script:

{
  "scripts": {
    "start": "node server.js",
    "test": "mocha test/**/*.js"
  },
  "devDependencies": {
    "chai": "^4.3.4",
    "chai-http": "^4.3.0",
    "mocha": "^9.1.3"
  },
  "dependencies": {
    "bcryptjs": "^2.4.3",
    "dotenv": "^15.0.2",
    "express": "^4.18.1",
    "express-rate-limit": "^6.4.0",
    "helmet": "^6.0.1",
    "mongoose": "^6.3.8",
    "cors": "^2.8.5",
    "jsonwebtoken": "^9.0.0"
  }
}

Step 5: Run the Tests:

Top 10 Interview Questions & Answers on NodeJS Best Practices and Interview Preparation

Top 10 Node.js Best Practices and Interview Preparation Questions

1. What are some key differences between Node.js and traditional server-side languages such as PHP or Java?

  • Event-driven and Non-blocking: Unlike traditional server-side languages which use a request-response threading model, Node.js operates asynchronously using an event loop.
  • Scalability: Thanks to its non-blocking architecture, Node.js can handle thousands of concurrent connections.
  • JavaScript Everywhere: Being based on JavaScript means the same language is used for both client and server, reducing context switching for developers.
  • Rich Ecosystem: Node.js benefits from a vast ecosystem with over a million packages available on npm (Node package manager).
  • Single-threaded: Node.js runs on a single thread using a worker pool that handles heavy tasks like file I/O in parallel. Traditional server-side languages often use multi-threading.

2. How does Node.js handle concurrency and what is an event loop?

Answer:

Node.js handles concurrency through an event-driven, non-blocking I/O model. The Event Loop is central to how this concurrency model works.

  • Event Loop Process:
    • The event loop continuously checks if there are any pending operations or callbacks.
    • When a task like an HTTP request or file read/write operation is initiated, it gets handed off to the appropriate system kernel module.
    • The kernel takes care of the operation and when it’s done, it adds the callback back to the event queue.
    • The event loop picks this callback from the queue and executes it.

This means Node.js can handle multiple requests at the same time efficiently without creating new threads for each request.

3. Can you describe middleware in Express.js and provide an example of how to write a custom middleware function?

Answer:

Middleware functions are functions that have access to the request object (req), the response object (res), and the next middleware function in the application’s request-response cycle. They are used to manipulate the request and/or response, execute any code, terminate the request-response cycle, or call the next middleware function.

Example of Custom Middleware:

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

function logRequest(req, res, next) {
    console.log(`${new Date().toISOString()} - ${req.method} request for '${req.url}'`);
    next();  // Passing control to the next handler
}

app.use(logRequest);

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

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

In this example, logRequest is a custom middleware function that logs the time, HTTP method, and URL path of every incoming request.

4. What is the difference between synchronous and asynchronous code in Node.js and why do we prefer async?

Answer:

In Node.js, synchronous code blocks the execution of subsequent lines of code until completion, which can lead to performance bottlenecks and unresponsiveness, especially during I/O operations. Asynchronous code, in contrast, allows the program to continue executing other parts without waiting for the previous operation to complete.

Example - Synchronous vs Asynchronous:

const fs = require('fs');

// Synchronous
try {
    const data = fs.readFileSync('./textfile.txt');
    console.log(data.toString());
} catch (err) {
    console.error(err);
}

// Asynchronous
fs.readFile('./textfile.txt', 'utf8', (err, data) => {
    if (err) {
        return console.error(err);
    }
    console.log(data);
});

The synchronous version blocks the thread until fs.readFileSync completes, while the asynchronous version continues executing the rest of the code immediately after the I/O operation is initiated.

5. Explain the role of package.json in a Node.js project.

Answer:

package.json is a JSON formatted file located at the root of every Node.js project. It contains important metadata about the project such as its name, version, dependencies, scripts, author(s), descriptions, etc.

Main Roles:

  • Dependencies Management: Lists the modules the project depends on and their versions.
  • Scripts Management: Allows you to define various commands for running, testing, and building your application.
  • Project Configuration: Provides a place to store configuration options for your application.
  • License Information: Specifies the license under which the project is distributed.
  • Versioning: Helps manage application versions and facilitates the release process.
  • Module Exports: If the project is a package itself, package.json can define its main entry point.

Example:

{
  "name": "example-app",
  "version": "1.0.0",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "express": "^4.17.1"
  }
}

6. How can you improve error handling in a Node.js application?

Answer:

Improper error handling can lead to crashes and security vulnerabilities. Here are some best practices:

  • Try-Catch Blocks: Use try-catch blocks particularly around asynchronous code where synchronous exceptions are likely.
  • Custom Error Classes: Create custom error classes to represent different types of errors.
  • Logging: Implement logging for error tracking during development and production.
  • Express Error Handling Middleware: Use error-handling middleware in Express to catch errors in all requests.
  • Promises & Async/Await: Use Promises properly and handle rejection cases by chaining .catch(). With async/await use try/catch blocks.
  • Global Error Handlers: Use process.on('uncaughtException') and process.on('unhandledRejection') for catching unexpected errors.

Example:

async function getUser(userId) {
    try {
        const user = await findUserById(userId);  // Assume findUserById is a Promise
        if (!user) {
            throw new Error('User not found');
        }
        return user;
    } catch (e) {
        console.error(e.message);
        throw e;
    }
}

7. Describe how to structure a large Node.js application using the MVC pattern.

Answer:

Model-View-Controller (MVC) is a great design pattern to structure larger applications for better management and organization.

  • Model: Manages data and business logic. In Node.js, this can be handled by an ORM or data access layer.
  • View: Handles presentation to the end-user. Could be an HTML template engine like Pug, EJS, Handlebars.
  • Controller: Receives input, invokes model and view functionalities. Acts as a mediator between Model and View components.

For example, using Express and Mongoose (for MongoDB):

// models/user.js
const mongoose = require('mongoose');
const Schema = mongoose.Schema;

const userSchema = new Schema({
  name: String,
  age: Number
});

module.exports = mongoose.model('User', userSchema);

// controllers/user.js
const User = require('../models/user');

exports.getAllUsers = async (req, res) => {
  try {
    const users = await User.find();
    res.json(users);
  } catch (error) {
    res.status(500).json({ message: error.message });
  }
};

// routes/user.js
const express = require('express');
const router = express.Router();
const userController = require('../controllers/user');

router.get('/', userController.getAllUsers);
module.exports = router;

// server.js
const express = require('express');
const mongoose = require('mongoose');
const userRoutes = require('./routes/user');  // Import route

const app = express();

// ... other setup and routes ...

app.use('/users', userRoutes);

// Connect to MongoDB
mongoose.connect('mongodb://localhost/mydatabase', { useNewUrlParser: true, useUnifiedTopology: true });

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

8. How do you secure a Node.js application against typical attacks?

Answer:

Securing Node.js applications involves several aspects including validation, protecting sensitive data, and preventing injection attacks.

  • Use Helmet.js: This helps secure HTTP headers.
  • Validation: Always validate inputs, sanitize data to prevent injection attacks.
  • Error Stack Traces: Avoid sending error stack traces to users.
  • Environment Variables: Store sensitive data in environment variables and never hardcode them.
  • Session Store Management: Use a secure session store like Redis instead of storing them in-memory.
  • Secure Password Hashing: Use libraries like bcrypt to hash passwords.
  • Rate Limiting: Implement rate limiting to prevent brute force attacks.
  • Input Sanitization: Sanitize inputs to protect against XSS attacks.
  • Regular Security Audits & Dependency Updates: Keep up-to-date with security vulnerabilities and updates in dependencies.

9. What is callback hell, and how can you avoid it in Node.js?

Answer:

Callback hell occurs when dealing with multiple nested callbacks making the code difficult to understand and maintain. It's often seen in I/O bound Node.js applications.

To avoid it, you can use:

  • Promises: A Promise is an object that represents the eventual completion (or failure) of an asynchronous operation. It avoids deep nesting by using .then() chains.
  • Async/Await: Introduced in ES2017, Async/Await makes asynchronous code look and behave a little more like synchronous code without actually blocking the thread.
  • Modular Code: Split code into separate modules/functions to remove nesting.

Example using Promises:

getUser(userId)
  .then(user => {
    return getOrderHistory(user.orderId);
  })
  .then(history => {
    return getInvoiceDetails(history.invoiceId);
  })
  .then(invoice => {
    return updateInvoiceStatus(invoice.id, 'paid');
  })
  .then(result => {
    res.send(result);
  })
  .catch(error => {
    console.error(error);
    res.status(500).send('Internal Server Error');
  });

Same Example with Async/Await:

app.get('/', async (req, res) => {
  try {
    const user = await getUser(userId);
    const history = await getOrderHistory(user.orderId);
    const invoice = await getInvoiceDetails(history.invoiceId);
    const result = await updateInvoiceStatus(invoice.id, 'paid');
    res.send(result);
  } catch (error) {
    console.error(error);
    res.status(500).send('Internal Server Error');
  }
});

10. How would you optimize a Node.js application for performance?

Answer:

Optimizing a Node.js application requires identifying and addressing bottlenecks that may exist within your codebase, server configurations, or third-party services.

  • Profiling & Monitoring: Use tools like New Relic, AppDynamics or built-in profiling tools in Node.js.
  • Efficient Data Usage: Use appropriate queries and indexing in database management.
  • Caching Mechanisms: Caching frequently accessed results reduces server load (Redis, Memcached as external cache).
  • Asynchronous Operations: Make sure operations are asynchronous, utilize promises/async-await constructs.
  • Streaming: For processing large files or data, use streaming APIs.
  • Load Balancing: Distribute traffic across multiple instances of the application.
  • Compression: Enable compression of responses (gzip).
  • Minification and Bundling: Use tools like Webpack or Minifier for frontend assets.
  • Cluster Module: Use Node.js cluster module to spawn multiple processes for handling higher loads.

Example of Caching:

You May Like This Related .NET Topic

Login to post a comment.