Backend (NodeJS) DeveloperKit
Backend (NodeJS) DeveloperKit
- Sample
Hello and welcome! I'm Parikh Jain, and I'm excited to share with you the
ultimate guide to become a MERN stack full stack developer. This kit is a labor
of love, drawn from my extensive journey as an SDE at Amazon, a founding
member at Coding Ninjas, and the founder of ProPeers. I’ve distilled my real-
world experience into a comprehensive resource that covers every topic you
need to excel.
5. Database Integration
9. Real-Time Communication
Target Audience
Aspiring Node.js Developers: Beginners who want to learn Node.js
fundamentals and build a strong foundation in backend development.
Hands-On Practice: Implement the provided code snippets and modify them
to suit your project requirements. Use the challenges as a starting point to
build more complex systems.
Reference Material: Use the Additional Resources section for further reading,
exploring useful Node.js packages, and staying updated with the latest trends.
Important Concepts
What is Node.js?
Event-Driven Architecture:
Node.js uses a non-blocking, event-driven architecture that makes it
lightweight and efficient—ideal for data-intensive real-time applications.
Full-Stack Integration:
Node.js seamlessly integrates with various frontend frameworks (like React,
Angular, or Vue) and databases (like MongoDB) to build modern full-stack
applications.
Expected Answer: Node.js is widely used for building REST APIs, real-time
applications (e.g., chat apps, live dashboards), microservices, single-page
applications (SPAs), and data streaming applications.
This introductory section sets the stage for the rest of the guide. It provides an
overview of what Node.js is, its key advantages, and how it fits into modern
backend development. The interview questions help assess your foundational
knowledge and understanding of the Node.js ecosystem.
Important Concepts
Node.js Runtime:
Node.js is a JavaScript runtime built on Chrome's V8 engine that allows
JavaScript to be executed on the server side.
npm is used to install, manage, and update Node.js packages and libraries.
Visit the Node.js official website and download the LTS version.
2. Verify Installation:
bash
Copy
node -v
npm -v
Important Concepts
Project Metadata:
npm Initialization:
Use npm init (or npm init -y for default settings) to generate the package.json file.
bash
Copy
npm init -y
Sample package.json
json
Copy
{
"name": "node-backend-project",
"version": "1.0.0",
"description": "A sample Node.js backend project",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js",
"test": "jest"
},
"author": "Your Name",
"license": "ISC",
"dependencies": {
"express": "^4.18.2"
},
"devDependencies": {
"nodemon": "^2.0.20",
Important Concepts
Installing Dependencies:
npm Scripts:
Scripts defined in package.json automate tasks like starting the server or running
tests.
Code Snippets
1. Install Express:
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js",
"test": "jest"
}
3. Running Scripts:
Important Concepts
Integrated Development Environments (IDEs):
Tools like Visual Studio Code (VS Code) and WebStorm provide rich features
(debugging, extensions, Git integration) for Node.js development.
Version Control:
Use Git to manage source code. Create a .gitignore file to exclude files such as
node_modules/ .
Code Snippets
1. VS Code Settings ( .vscode/settings.json ):
{
"editor.formatOnSave": true,
"eslint.alwaysShowStatus": true,
"gitlens.advanced.messages": {
"suppressShowKeyBindingsNotice": true}
2. Git Setup:
git init
node_modules/
.env
git add .
git commit -m "Initial commit"
git push origin main
Answer:
node -v
npm -v
Expected Output: Version numbers for both Node.js and npm confirm proper
installation.
Answer:
Explanation: npm scripts automate common tasks like starting the server,
running tests, or launching development tools. They are defined in the scripts
section of package.json and can be run using npm start or npm run <script-name> .
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js",
"test": "jest"
}
Run Script:
npm start
Express Instance:
Middleware:
Functions that process requests and responses before reaching your route
handlers.
// server.js
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;
Routing:
Middleware Functions:
Functions that intercept and process requests before passing them on to the
next handler.
// routes/greeting.js
const express = require('express');
const router = express.Router();
// server.js (continued)
const greetingRoute = require('./routes/greeting');
app.use('/api', greetingRoute);
// middleware/logger.js
function logger(req, res, next) {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
next();
}
module.exports = logger;
Usage in server.js:
// middleware/errorHandler.js
function errorHandler(err, req, res, next) {
console.error('Error:', err.stack);
res.status(500).json({ error: 'Something went wrong!' });
}
module.exports = errorHandler;
router.use() applies middleware only to the routes defined within that router.
Code Example:
// Router-specific middleware
const router = express.Router();
router.use((req, res, next) => {
console.log('Router-specific middleware');
next();
});
router.get('/test', (req, res) => res.send('Test route'));
app.use('/api', router);
Answer:
You can create separate router modules and use app.use() to mount them on
specific paths.
// In routes/users.js
const express = require('express');
const router = express.Router();
module.exports = router;
// In server.js
const usersRouter = require('./routes/users');
app.use('/api/users', usersRouter);
HTTP Methods:
Status Codes:
Use proper HTTP status codes (e.g., 200, 201, 400, 404, 500) to communicate
the outcome of API requests.
Data Validation:
API Versioning:
Organize endpoints into versions (e.g., /api/v1/ ) to handle changes over time.
// routes/users.js
const express = require('express');
const router = express.Router();
// server.js
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;
// Global error handler (optional, see next section for more on error handling)
app.use((err, req, res, next) => {
console.error('Error:', err.stack);
res.status(500).json({ error: 'Internal Server Error' });
});
Code Example:
router.post('/',
body('name').notEmpty().withMessage('Name is required'),
body('email').isEmail().withMessage('Valid email is required'),
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// Proceed to create user...
}
);
Code Example:
5. Database Integration
Modern Node.js backend applications often use NoSQL databases like MongoDB
for flexibility and scalability. Mongoose is a popular ODM (Object Data Modeling)
library that simplifies working with MongoDB in Node.js.
MongoDB:
A NoSQL database that stores data in JSON-like documents.
Mongoose:
An ODM that provides a straightforward schema-based solution to model
application data.
// db.js
const mongoose = require('mongoose');
module.exports = connectDB;
Schema Definition:
Define the structure of your documents using Mongoose schemas.
Models:
Create models based on schemas to interact with the corresponding
MongoDB collections.
// models/User.js
const mongoose = require('mongoose');
Express Integration:
Use Mongoose within Express routes to handle database operations.
// routes/users.js
const express = require('express');
const router = express.Router();
const User = require('../models/User');
// Get user by ID
router.get('/:id', async (req, res, next) => {
try {
const user = await User.findById(req.params.id);
if (!user) return res.status(404).json({ error: 'User not found' });
res.status(200).json(user);
} catch (err) {
next(err);
}
});
// Update user by ID
router.put('/:id', async (req, res, next) => {
try {
const updatedUser = await User.findByIdAndUpdate(
req.params.id,
req.body,
{ new: true, runValidators: true }
// Delete user by ID
router.delete('/:id', async (req, res, next) => {
try {
const deletedUser = await User.findByIdAndDelete(req.params.id);
if (!deletedUser) return res.status(404).json({ error: 'User not found' });
res.status(200).json(deletedUser);
} catch (err) {
next(err);
}
});
module.exports = router;
Relationships:
Embedding vs. referencing documents.
Middleware (Hooks):
Pre/post hooks in Mongoose to perform actions before or after operations.
// models/Post.js
const mongoose = require('mongoose');
In the User model, you can reference posts (if needed) or simply populate posts in
your queries:
Code Example:
User.find().skip(skip).limit(limit)
.then(users => res.json(users))
.catch(err => next(err));
Code Example:
UserSchema.pre('save', function(next) {
console.log('Before saving user:', this);
next();
});
// routes/auth.js
const express = require('express');
const jwt = require('jsonwebtoken');
const router = express.Router();
const secret = process.env.JWT_SECRET || 'your_jwt_secret';
module.exports = router;
// middleware/authenticate.js
const jwt = require('jsonwebtoken');
const secret = process.env.JWT_SECRET || 'your_jwt_secret';
module.exports = authenticateToken;
// routes/protected.js
const express = require('express');
const router = express.Router();
const authenticateToken = require('../middleware/authenticate');
module.exports = router;
bcrypt:
A widely used library for hashing and comparing passwords securely.
// utils/hashPassword.js
const bcrypt = require('bcrypt');
// Example usage:
hashPassword('myPlainPassword')
.then(hashed => console.log('Hashed password:', hashed))
.catch(err => console.error(err));
module.exports = hashPassword;
// utils/verifyPassword.js
const bcrypt = require('bcrypt');
// Example usage:
const plain = 'myPlainPassword';
const hashed = '$2b$10$D4G5f18o7aMMfwasBlh6Lu...'; // Example hash
verifyPassword(plain, hashed)
.then(match => console.log('Password match:', match))
.catch(err => console.error(err));
module.exports = verifyPassword;
// routes/usersAuth.js
const express = require('express');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const router = express.Router();
const secret = process.env.JWT_SECRET || 'your_jwt_secret';
// User Registration
router.post('/register', async (req, res) => {
const { username, password, email } = req.body;
const hashedPassword = await bcrypt.hash(password, 10);
const newUser = { id: users.length + 1, username, email, password: hashedP
assword };
users.push(newUser);
res.status(201).json({ message: 'User registered successfully' });
});
// User Login
router.post('/login', async (req, res) => {
const { username, password } = req.body;
const user = users.find(u => u.username === username);
if (!user) return res.status(400).json({ error: 'User not found' });
module.exports = router;
Code Example:
function generateToken(user) {
return jwt.sign({ id: user.id, username: user.username }, secret, { expiresI
n: '1h' });
}
Code Example:
(See the authenticateToken middleware snippet in Part 6.1.1.)
Interview Question 4
Q: How do you protect a route so that only authenticated users can access it?
Answer:
Code Example:
Jest:
// utils/math.js
function add(a, b) {
return a + b;
}
module.exports = { add };
// tests/math.test.js
const { add } = require('../utils/math');
npm test
Supertest:
A library for testing HTTP endpoints in Node.js by simulating requests.
// server.js
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;
app.use(express.json());
module.exports = app;
// tests/api.test.js
const request = require('supertest');
const app = require('../server');
TDD:
Write tests before writing the actual code. This ensures that your code meets
the specified requirements.
Cycle:
Red (fail) → Green (pass) → Refactor.
// tests/counter.test.js
const { increment } = require('../utils/counter');
test('increment should add 1 to the number', () => {
expect(increment(1)).toBe(2);
});
// utils/counter.js
function increment(n) {
return n + 1;
}
CI Pipelines:
Automate tests on every push/commit using tools like GitHub Actions or
Jenkins.
# .github/workflows/nodejs.yml
name: Node.js CI
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x, 16.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
- run: npm install
- run: npm test
Code Example:
Answer:
Use Jest’s built-in mocking functions to simulate external modules.
Code Example:
Caching:
Storing frequently accessed data in memory to reduce latency and improve
throughput.
Redis:
An in-memory data structure store, used as a cache, message broker, and
more.
// redisClient.js
const redis = require('redis');
const client = redis.createClient({ host: '127.0.0.1', port: 6379 });
client.on('connect', () => {
console.log('Connected to Redis');
});
module.exports = client;
// routes/data.js
const express = require('express');
const router = express.Router();
const redisClient = require('../redisClient');
module.exports = router;
Lean Queries:
In MongoDB/Mongoose, using .lean() returns plain JavaScript objects instead
of Mongoose documents, reducing overhead.
Code Profiling:
Use tools like Node.js built-in profiler or external tools (e.g., Clinic.js) to
identify bottlenecks.
Rate Limiting:
Prevents abuse and ensures fair usage by limiting the number of requests per
client within a specified time window.
express-rate-limit:
A middleware to apply rate limiting on Express routes.
1. Installation:
// middleware/rateLimiter.js
const rateLimit = require('express-rate-limit');
Code Example:
9. Real-Time Communication
Part 9.1: Concepts & Code Snippets
Key Concepts
Real-Time Communication:
Enables bi-directional, persistent communication between clients and the
server.
WebSockets:
A protocol that provides full-duplex communication channels over a single
TCP connection.
Socket.io:
A Node.js library that simplifies real-time communication by providing
fallbacks (like long polling) when WebSockets are not available, as well as
additional features such as automatic reconnection, rooms, and namespaces.
Handling Connections:
Managing client connections, disconnections, and error events.
// server.js
const express = require('express');
const http = require('http');
// Handle disconnection
socket.on('disconnect', () => {
console.log('Client disconnected:', socket.id);
});
});
socket.on('connect', () => {
console.log('Connected to server via Socket.io');
});
socket.on('disconnect', () => {
console.log('Client disconnected:', socket.id);
});
});
Explanation: Use the socket.io-client library in your test suite to simulate client
connections and assert events.
Code Example:
socket.on('connect', () => {
console.log('Test client connected');
socket.emit('message', 'Test Message');
});
Docker:
Containerization allows you to package your application and its dependencies
into a single image that runs consistently in any environment.
# Install dependencies
RUN npm install
# docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=production
redis:
image: redis:alpine
ports:
- "6379:6379"
CI/CD:
Continuous Integration and Continuous Deployment automate testing, building,
and deploying your application on code changes.
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x, 16.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
- run: npm install
- run: npm test
- run: npm run build # if applicable
# Optionally, deploy steps can be added here
pipeline {
agent any
stages {
stage('Checkout') {
steps {
checkout scm
}
}
Winston:
PM2:
A process manager that helps you run, monitor, and manage Node.js
applications in production.
// utils/logger.js
const { createLogger, format, transports } = require('winston');
module.exports = logger;
// ecosystem.config.js
module.exports = {
apps: [{
Code Example:
// routes/kv.js
const express = require('express');
const router = express.Router();
const store = {};
module.exports = router;
// lruCache.js
class LRUCache {
constructor(capacity) {
this.capacity = capacity;
this.cache = new Map();
}
get(key) {
put(key, value) {
if (this.cache.has(key)) {
this.cache.delete(key);
} else if (this.cache.size >= this.capacity) {
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
this.cache.set(key, value);
}
}