Skip to main content

1. MongoDB Fundamentals

MongoDB is a NoSQL document database that stores data in flexible JSON-like documents. Key features include schema flexibility, horizontal scalability, rich query language, indexing support, aggregation framework, and replication for high availability.
BSON (Binary JSON) is MongoDB’s binary-encoded format. It extends JSON with additional data types like Date, Binary, ObjectId, and provides more efficient encoding/decoding and traversal.
Collections are groups of MongoDB documents, similar to tables in relational databases. Documents are records in BSON format, consisting of field-value pairs with flexible schemas.
Create: insertOne(), insertMany(). Read: find(), findOne(). Update: updateOne(), updateMany(), replaceOne(). Delete: deleteOne(), deleteMany(). These operations form the foundation of MongoDB interactions.
The _id field is the primary key in MongoDB documents, automatically generated as ObjectId if not provided. It’s unique within a collection and indexed by default for fast lookups.

2. MongoDB Advanced Concepts

Indexing creates data structures for faster queries. Types include single field, compound, multikey, text, and geospatial indexes. Indexes improve query performance but increase write overhead and storage.
Aggregation processes data through a pipeline of stages like $match, $group, $project, $sort, $lookup. It enables complex data transformations and analytics directly in the database.
Sharding distributes data across multiple servers for horizontal scaling. It enables handling large datasets by partitioning data based on a shard key across multiple shards.
Replica sets are groups of MongoDB instances maintaining the same data for high availability. They include one primary (writes) and multiple secondaries (replication) with automatic failover.
Embedded: related data within the same document (denormalized). Referenced: separate documents linked by IDs (normalized). Choice depends on access patterns and data size.

3. Express.js Basics and Middleware

Express.js is a minimal, flexible Node.js web framework providing robust features for web and mobile applications with a thin layer over Node.js’s http module.
Middleware functions have access to req, res, and next. They execute code, modify req/res, end cycles, or call next(). Types include application-level, router-level, error-handling, and third-party.
Routing defines how applications respond to client requests at specific endpoints (URIs) and HTTP methods. Express provides app.METHOD(PATH, HANDLER) syntax for route definition.
Route parameters capture values from URL segments (/users/:id) accessible via req.params. Query strings (?key=value) are accessed via req.query for optional parameters.
Use express.json() and express.urlencoded() middleware to parse request bodies. Access parsed data via req.body in route handlers.
Express.js is a minimal and flexible Node.js web framework that simplifies handling HTTP requests, routing, and middleware integration. It acts as a wrapper around Node’s HTTP module, providing a clean abstraction to build REST APIs and web applications.

Example:

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

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

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

Why:

Because Node’s native HTTP module is low-level. Express provides convenient abstractions like routing, middleware, and request parsing.

When:

Use it when building REST APIs, microservices, or web apps that require request handling.

Where:

It’s the most common framework in production Node.js environments.
Middleware functions are functions that execute during the request-response cycle. They have access to req, res, and next() — used to modify requests, handle authentication, validation, logging, etc.

Example:

function logger(req, res, next) {
  console.log(`${req.method} ${req.url}`);
  next(); // Pass control to next middleware
}

app.use(logger);
app.get('/', (req, res) => res.send('Home'));

Why:

Middleware provides a way to modularize logic — logging, authentication, validation — without polluting route handlers.

When:

Use middleware whenever you want to perform an action before reaching your route (or after response, like error logging).

Where:

Common examples:
  • express.json() for parsing JSON
  • cors() for handling cross-origin requests
  • Custom middleware for auth/validation
TypeDescriptionExample
Application-levelBound to appapp.use()
Router-levelBound to an Express routerrouter.use()
Built-inProvided by Expressexpress.json(), express.static()
Third-partyInstalled via npmcors, helmet, morgan
Error-handling4 parameters (err, req, res, next)Custom error middleware

Example:

// Error-handling middleware
app.use((err, req, res, next) => {
  console.error(err.message);
  res.status(500).send('Internal Server Error');
});

Why:

To create reusable, layered logic pipelines.

When:

Whenever you need pre/post-processing around route handlers.

Where:

Throughout the Express lifecycle (before/after routes).
next() passes control to the next middleware in the stack. If not called, the request hangs (no response sent).

Example:

app.use((req, res, next) => {
  console.log('Before route');
  next(); // Pass to next middleware or route
});

app.get('/', (req, res) => {
  res.send('After middleware');
});

Why:

To chain multiple middlewares for modular logic.

When:

When you need layered control (logging → validation → controller).

Where:

Always in custom middleware unless it ends the request.
FunctionPurpose
app.use()Registers middleware (runs for all methods unless filtered by path)
app.get()Defines a route handler specifically for GET requests

Example:

app.use('/users', (req, res, next) => {
  console.log('Middleware for /users');
  next();
});

app.get('/users', (req, res) => res.send('User list'));

Why:

use() is for pre-processing, get() is for request handling.

When:

Use use() for shared logic like authentication.

Where:

Across all routes or specific prefixes.
  1. Request enters Express
  2. Passes through middleware stack
  3. If matched, route handler executes
  4. If no match → 404 handler
  5. If error → error-handling middleware

Example Flow:

Incoming Request

app.use(logger)

app.use(auth)

app.get('/users', controller)

app.use(errorHandler)

Response Sent

Why:

Understanding this helps you debug request flow and performance.

When:

When analyzing middleware order or debugging missed routes.

Where:

In Express core — it’s what powers routing and middleware sequencing.
By using a custom error-handling middleware — identified by 4 parameters (err, req, res, next).

Example:

app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({ message: err.message });
});

// And to trigger it:
app.get('/', (req, res, next) => {
  next(new Error('Something went wrong'));
});

Why:

Centralized error handling avoids repeating try/catch everywhere.

When:

In production APIs, always define a global error middleware.

Where:

Place at the bottom of middleware stack (after routes).
MethodPurpose
res.send()Sends response (auto-detects type)
res.json()Sends JSON response (sets content-type)
res.end()Ends response without data

Example:

res.send('Hello');
res.json({ success: true });
res.end(); // No data, just closes connection

Why:

Each method suits different scenarios (raw text vs structured data).

When:

Use:
  • send() → text or HTML
  • json() → APIs
  • end() → streams or empty responses

Where:

Controllers or route handlers.
Key practices:
  1. Use Helmet → sets HTTP headers
  2. Use Rate-limiter → prevent brute-force
  3. Use CORS properly
  4. Use express.json({ limit }) → prevent payload overflows
  5. Disable x-powered-by header
  6. Validate all inputs (with Joi/Zod)

Example:

const helmet = require('helmet');
const rateLimit = require('express-rate-limit');

app.use(helmet());
app.use(rateLimit({ windowMs: 60 * 1000, max: 100 }));
app.disable('x-powered-by');

Why:

Express is not secure by default; hardening prevents XSS, CSRF, and DoS attacks.

When:

Always in production environments.

Where:

At app initialization or middleware level.
Async functions throw errors inside Promises, so you must use:
  1. Try/catch inside route, or
  2. Wrap with async error handler.

Example:

// Approach 1: try/catch
app.get('/', async (req, res, next) => {
  try {
    const users = await User.find();
    res.json(users);
  } catch (err) {
    next(err);
  }
});

// Approach 2: wrapper function
const asyncHandler = fn => (req, res, next) =>
  Promise.resolve(fn(req, res, next)).catch(next);

Why:

Express doesn’t automatically catch async errors without .catch().

When:

Always for async routes (DB calls, APIs).

Where:

Use asyncHandler pattern across all async routes.
Express maintains a router stack for every HTTP method and path. When a request comes in, it iterates through this stack and executes the first matching route or middleware.

Example:

app.get('/users', handler1);
app.use('/users', handler2);
The router internally uses the path-to-regexp library to match routes efficiently.

Why:

To provide performant routing and predictable middleware flow.

When:

When debugging routing conflicts or order issues.

Where:

Inside express/lib/router/layer.js and route.js source.
Route parameters are dynamic values in URL paths, accessible via req.params.

Example:

app.get('/users/:id', (req, res) => {
  res.send(`User ID: ${req.params.id}`);
});

Why:

Used to identify specific resources (user by ID, post by slug).

When:

For REST endpoints like /users/:id, /posts/:slug.

Where:

Inside route handlers.

4. Express.js Authentication & Authorization

  • Authentication → verifying identity (e.g., login with email/password or Google OAuth)
  • Authorization → verifying permissions (e.g., only admin can delete a user)

Example:

// Example route using both
app.get('/admin', authenticateUser, authorizeRole('admin'), (req, res) => {
  res.send('Welcome Admin!');
});

Why:

Separating these concerns improves scalability and security.

When:

Always implement authenticate first, then apply authorize on protected routes.

Where:

Middleware layer is the best place — reusable across routes.
JWT (JSON Web Token) is a stateless authentication mechanism — no need to store sessions in the database.

Steps:

  1. User logs in with credentials
  2. Server verifies credentials and issues a signed JWT using jsonwebtoken
  3. Client stores JWT (usually in localStorage or cookie)
  4. Client sends JWT in Authorization: Bearer <token> header for protected routes

Example:

import jwt from 'jsonwebtoken';

// Generate Token
const token = jwt.sign({ userId: user.id, role: user.role }, process.env.JWT_SECRET, {
  expiresIn: '1h',
});

// Middleware for verification
function verifyToken(req, res, next) {
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) return res.status(401).json({ error: 'No token provided' });

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded;
    next();
  } catch (err) {
    res.status(403).json({ error: 'Invalid token' });
  }
}

Why:

JWT avoids database lookups on every request (unlike sessions).

When:

Use JWT for REST APIs and microservices.

Where:

Ideal for stateless systems or distributed apps (like mobile + web).
Use sessions when:
  • The app is monolithic and server-rendered (like an Express + EJS app)
  • You need easy session invalidation (e.g., logout all sessions)
  • You store user-specific state in the backend

Example (session-based login):

import session from 'express-session';

app.use(session({
  secret: 'keyboard cat',
  resave: false,
  saveUninitialized: true,
  cookie: { secure: false },
}));

Why:

Sessions allow you to track user data server-side and revoke tokens instantly.

When:

In traditional web apps or dashboards.

Where:

Stored in Redis for scalability.
RBAC ensures only specific roles can perform actions.

Example:

function authorizeRole(...roles) {
  return (req, res, next) => {
    if (!roles.includes(req.user.role)) {
      return res.status(403).json({ error: 'Access denied' });
    }
    next();
  };
}

// Usage
app.delete('/users/:id', verifyToken, authorizeRole('admin'), deleteUser);

Why:

Prevents unauthorized operations from lower-privileged users.

When:

In systems with hierarchical roles (admin, manager, user).

Where:

Middleware layer.
ConceptRBACABAC
Full formRole-Based Access ControlAttribute-Based Access Control
Based onUser roleUser + Resource attributes
FlexibilityStaticDynamic (context-aware)

Example (ABAC):

// Only allow if user owns the resource
if (req.user.id !== post.authorId) {
  return res.status(403).json({ error: 'Not allowed' });
}

Why:

ABAC allows more granular control.

When:

Use ABAC in enterprise apps needing dynamic policies (like file sharing).

Where:

Applied at business logic level.
Never store plain text passwords — always hash and salt them.

Example using bcrypt:

import bcrypt from 'bcrypt';

const salt = await bcrypt.genSalt(10);
const hashedPassword = await bcrypt.hash(password, salt);

Why:

Hashing protects passwords even if the database is compromised.

When:

During user registration or password change.

Where:

Inside your user service layer before saving to DB.
Access tokens expire quickly (e.g., 15m). Refresh tokens generate new access tokens securely.

Example:

// Generate both
const accessToken = jwt.sign({ userId }, JWT_SECRET, { expiresIn: '15m' });
const refreshToken = jwt.sign({ userId }, REFRESH_SECRET, { expiresIn: '7d' });

// Refresh endpoint
app.post('/refresh', (req, res) => {
  const { token } = req.body;
  const user = jwt.verify(token, REFRESH_SECRET);
  const newAccessToken = jwt.sign({ userId: user.id }, JWT_SECRET, { expiresIn: '15m' });
  res.json({ accessToken: newAccessToken });
});

Why:

Improves security while maintaining usability.

When:

For long-lived sessions (like mobile apps).

Where:

Store refresh tokens in DB or secure cookies.
Since JWTs are stateless, you can’t just “delete” them.

Common strategies:

  1. Blacklist tokens in Redis
  2. Rotate refresh tokens (invalidate old ones)
  3. Use short expiry times for access tokens

Example:

// Blacklist token on logout
redisClient.setex(token, expiryTime, 'blacklisted');

Why:

Prevents reuse of stolen tokens.

When:

During logout or detected suspicious activity.

Where:

Store in Redis for quick lookup.
  1. JWT token leakage — store tokens securely
  2. No HTTPS — exposes credentials
  3. Brute force attacks — use rate limiting
  4. Insecure password reset links — use short-lived tokens

Example mitigation:

import rateLimit from 'express-rate-limit';
app.use('/auth/login', rateLimit({ windowMs: 15 * 60 * 1000, max: 5 }));

Why:

Authentication is a top security target.

When:

Apply protections globally.

Where:

Security middleware layer.
Use OAuth 2.0 and libraries like passport.js.

Example:

import passport from 'passport';
import { Strategy as GoogleStrategy } from 'passport-google-oauth20';

passport.use(new GoogleStrategy({
  clientID: process.env.GOOGLE_ID,
  clientSecret: process.env.GOOGLE_SECRET,
  callbackURL: '/auth/google/callback',
}, (accessToken, refreshToken, profile, done) => {
  done(null, profile);
}));

Why:

Simplifies user onboarding.

When:

In consumer apps (like e-commerce or SaaS).

Where:

Handled by auth microservice or route handler.

5. React Fundamentals & Core Concepts

React is a JavaScript library for building user interfaces with component-based architecture, virtual DOM, one-way data flow, and JSX syntax.
JSX is syntax extension allowing HTML-like code in JavaScript. It transpiles to React.createElement() calls, providing intuitive UI description with JavaScript expressions.
Components are independent, reusable UI pieces. Functional components are preferred: const MyComponent = (props) => {props.text}. They accept props and return React elements.
Props are read-only inputs passed from parent to child components. They enable data flow and component communication: <Component prop="value" />.
State stores component’s dynamic data. Use useState hook: const [count, setCount] = useState(0). State updates trigger re-renders. Never mutate state directly.
The Virtual DOM (VDOM) is a lightweight copy of the real DOM that React keeps in memory. When a component’s state or props change:
  • React creates a new virtual DOM tree
  • It diffs it with the previous one (using a diffing algorithm)
  • It updates only the changed elements in the real DOM — improving performance

Code Example:

import React, { useState } from "react";

export default function Counter() {
  const [count, setCount] = useState(0);

  // React updates only the <p> element, not the entire DOM
  return (
    <div>
      <h1>Virtual DOM Example</h1>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

Why it matters:

Directly updating the DOM is expensive. The Virtual DOM allows React to minimize those updates efficiently.
  • Controlled Component: React controls the input’s value using state
  • Uncontrolled Component: The DOM handles the input’s value directly via ref

Code Example (Controlled):

import { useState } from "react";

function ControlledInput() {
  const [value, setValue] = useState("");

  return (
    <input
      value={value}
      onChange={(e) => setValue(e.target.value)}
      placeholder="Controlled"
    />
  );
}

Code Example (Uncontrolled):

import { useRef } from "react";

function UncontrolledInput() {
  const inputRef = useRef();

  const handleSubmit = () => {
    alert(inputRef.current.value);
  };

  return (
    <>
      <input ref={inputRef} placeholder="Uncontrolled" />
      <button onClick={handleSubmit}>Submit</button>
    </>
  );
}
Controlled is best for validation or dynamic UI; uncontrolled can be used for performance or simple forms.
Hooks are special functions that let you use React features (state, lifecycle, context, etc.) in function components.
  • useEffect allows you to perform side effects (like data fetching, DOM updates, event listeners)
  • The dependency array controls when the effect runs

Code Example:

import { useEffect, useState } from "react";

function FetchUser() {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetch("https://jsonplaceholder.typicode.com/users/1")
      .then((res) => res.json())
      .then(setUser);
  }, []); // runs once after component mounts

  return <p>{user ? user.name : "Loading..."}</p>;
}

Behavior based on dependencies:

  • [] → runs once (on mount)
  • [var] → runs when var changes
  • no array → runs on every render
Without a unique key, React cannot properly identify which items changed, were added, or removed — leading to:
  • Unnecessary re-renders
  • Incorrect UI updates

Code Example:

function ListExample({ items }) {
  return (
    <ul>
      {items.map((item) => (
        // Always provide a unique key!
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
}

Bad Example (causes re-render issues):

<li key={index}>{item.name}</li>
If the list order changes, items may be mismatched when using index as key.
When multiple child components need to share or sync state, the state is “lifted up” to their common parent. This makes one source of truth for shared data.

Code Example:

function Parent() {
  const [value, setValue] = useState("");

  return (
    <>
      <ChildInput value={value} setValue={setValue} />
      <DisplayValue value={value} />
    </>
  );
}

function ChildInput({ value, setValue }) {
  return <input value={value} onChange={(e) => setValue(e.target.value)} />;
}

function DisplayValue({ value }) {
  return <p>You typed: {value}</p>;
}

Why it’s important:

Prevents inconsistent state across components.
  • Props Drilling: Passing props manually through multiple nested components
  • Context API: Allows data to be shared globally without passing props at every level

Props Drilling Example:

function GrandParent() {
  const theme = "dark";
  return <Parent theme={theme} />;
}

function Parent({ theme }) {
  return <Child theme={theme} />;
}

function Child({ theme }) {
  return <p>Theme is {theme}</p>;
}

Context API Example:

import { createContext, useContext } from "react";

const ThemeContext = createContext("light");

function GrandParent() {
  return (
    <ThemeContext.Provider value="dark">
      <Child />
    </ThemeContext.Provider>
  );
}

function Child() {
  const theme = useContext(ThemeContext);
  return <p>Theme is {theme}</p>;
}

When to use Context:

When data (like theme, user, locale) is needed globally or deeply nested.
Use memoization and avoid unnecessary renders:
  • React.memo() → prevents re-render if props haven’t changed
  • useMemo() → memoizes values
  • useCallback() → memoizes functions
  • Avoid anonymous functions inside render if possible

Code Example:

import React, { useState, useCallback, memo } from "react";

const Child = memo(({ onClick }) => {
  console.log("Child rendered");
  return <button onClick={onClick}>Click</button>;
});

function Parent() {
  const [count, setCount] = useState(0);
  const increment = useCallback(() => setCount((c) => c + 1), []);

  return (
    <>
      <p>Count: {count}</p>
      <Child onClick={increment} />
    </>
  );
}

Result:

Child won’t re-render unnecessarily because onClick reference stays stable.
Custom Hooks allow you to reuse logic between components. You create one when multiple components share the same logic (e.g., fetching, resizing, authentication).

Code Example:

import { useState, useEffect } from "react";

function useFetch(url) {
  const [data, setData] = useState(null);

  useEffect(() => {
    fetch(url)
      .then((res) => res.json())
      .then(setData);
  }, [url]);

  return data;
}

// Usage
function User() {
  const user = useFetch("https://jsonplaceholder.typicode.com/users/1");
  return <p>{user ? user.name : "Loading..."}</p>;
}

When to use:

When you want to abstract and share reusable React logic without duplicating code.

6. NEXT JS

FeaturePages Router (pages/)App Router (app/)
IntroducedBefore Next.js 13Next.js 13+
RenderingCSR, SSR, SSGRSC, SSR, SSG, ISR
File-based routingYesYes (with nested layouts)
Data fetchinggetServerSideProps, getStaticPropsAsync components or fetch directly
LayoutsCustomBuilt-in persistent layouts
Server Components✅ Default
Client Components✅ (with “use client”)

Example (App Router):

// app/page.tsx
export default async function HomePage() {
  const data = await fetch("https://api.example.com/posts").then(res => res.json());
  return <div>{data.map(p => <p key={p.id}>{p.title}</p>)}</div>;
}

Example (Pages Router):

// pages/index.js
export async function getServerSideProps() {
  const res = await fetch("https://api.example.com/posts");
  const data = await res.json();
  return { props: { data } };
}
export default function Home({ data }) {
  return <div>{data.map(p => <p key={p.id}>{p.title}</p>)}</div>;
}
  • Server Components (default):
    • Run on the server (never in browser)
    • Can fetch data directly
    • Reduce bundle size and improve performance
  • Client Components:
    • Run in the browser
    • Must be marked with “use client”
    • Can use useState, useEffect, event handlers, etc.

Example:

// app/page.tsx (Server Component)
import ClientButton from "./ClientButton";

export default async function Page() {
  const data = await fetch("https://api.example.com/user").then(r => r.json());
  return (
    <div>
      <h1>Hello, {data.name}</h1>
      <ClientButton />
    </div>
  );
}

// app/ClientButton.tsx (Client Component)
"use client";
import { useState } from "react";

export default function ClientButton() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(count + 1)}>Clicked {count}</button>;
}
ModeDescriptionWhen to Use
SSR (Server-Side Rendering)Page rendered at every requestDynamic data that changes often (e.g., dashboard)
SSG (Static Site Generation)Page pre-rendered at build timeStatic content (e.g., blog, docs)
ISR (Incremental Static Regeneration)SSG + revalidation after a time intervalSemi-static content (e.g., news feed)

Example (ISR):

// app/blog/page.tsx
export const revalidate = 60; // regenerate every 60 seconds

export default async function BlogPage() {
  const posts = await fetch("https://api.example.com/posts").then(res => res.json());
  return <div>{posts.map(p => <p key={p.id}>{p.title}</p>)}</div>;
}
  • SSR: When SEO and fast first paint are important
  • CSR (Client-Side Rendering): When user-specific or highly interactive data is needed (like dashboards)

Example CSR (client fetch):

"use client";
import { useEffect, useState } from "react";

export default function Dashboard() {
  const [data, setData] = useState([]);

  useEffect(() => {
    fetch("/api/data").then(res => res.json()).then(setData);
  }, []);

  return <div>{data.map(item => <p key={item.id}>{item.name}</p>)}</div>;
}
In App Router, API routes live under /app/api/*/route.ts.

Example:

// app/api/user/route.ts
import { NextResponse } from "next/server";

export async function GET() {
  const user = { name: "Shehroze", age: 28 };
  return NextResponse.json(user);
}

export async function POST(req: Request) {
  const body = await req.json();
  return NextResponse.json({ message: "User created", data: body });
}

Usage:

const res = await fetch("/api/user");
const user = await res.json();
Middleware runs before a request is processed. It can:
  • Redirect or rewrite requests
  • Check authentication
  • Modify headers
Runs on: Edge Runtime (very fast, lightweight)

Example:

// middleware.ts
import { NextResponse } from "next/server";

export function middleware(req) {
  const isLoggedIn = req.cookies.get("token");
  const url = req.nextUrl.clone();

  if (!isLoggedIn && url.pathname.startsWith("/dashboard")) {
    url.pathname = "/login";
    return NextResponse.redirect(url);
  }
}
generateMetadata() allows dynamic SEO metadata generation per page.

Example:

// app/blog/[slug]/page.tsx
export async function generateMetadata({ params }) {
  const post = await fetch(`https://api.example.com/posts/${params.slug}`).then(r => r.json());
  return {
    title: post.title,
    description: post.excerpt,
  };
}

export default function BlogPost({ params }) {
  return <h1>Post: {params.slug}</h1>;
}
Benefit: SEO-friendly and supports dynamic OG tags, locales, etc.
  • Layouts: Persistent UI (e.g., navbars, sidebars) wrapping child routes
  • Parallel Routes: Allow rendering multiple route segments simultaneously (e.g., tabs)

Layout Example:

// app/layout.tsx
export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <nav>Navigation</nav>
        {children}
      </body>
    </html>
  );
}

Parallel Routes Example:

// app/@tabs/(profile)/page.tsx
export default function ProfileTab() {
  return <p>Profile Tab</p>;
}

// app/@tabs/(settings)/page.tsx
export default function SettingsTab() {
  return <p>Settings Tab</p>;
}
Next.js <Image> component optimizes images automatically:
  • Lazy loading
  • Responsive resizing
  • WebP conversion

Example:

import Image from "next/image";

export default function Hero() {
  return (
    <Image
      src="/hero.jpg"
      alt="Hero Banner"
      width={1200}
      height={600}
      priority
    />
  );
}
Benefit: Automatically reduces payload and improves Core Web Vitals.
Next.js has built-in i18n support in config and route-level solutions.

Example using next-intl:

// middleware.ts
import createMiddleware from "next-intl/middleware";

export default createMiddleware({
  locales: ["en", "ar"],
  defaultLocale: "en",
});

Usage:

// app/[locale]/page.tsx
import { useTranslations } from "next-intl";

export default function HomePage() {
  const t = useTranslations("Home");
  return <h1>{t("welcome")}</h1>;
}
Result: Automatically renders pages in the selected locale.

7. React State Management and Advanced Patterns

Code splitting breaks bundle into chunks loaded on demand. Use React.lazy() for component-level splitting: const LazyComponent = React.lazy(() => import('./Component')).
Error boundaries catch JavaScript errors in component tree. Implement with componentDidCatch and getDerivedStateFromError. Display fallback UI instead of crashing.
Portals render children outside parent DOM hierarchy: ReactDOM.createPortal(child, container). Useful for modals, tooltips, overlays maintaining React tree relationship.
React Router enables client-side routing in SPAs. Define routes with <Route path="/" element={<Component />} />. Navigate with Link or useNavigate hook.
Techniques: React.memo, useCallback, useMemo, code splitting, virtualization, lazy loading, avoid inline functions, proper keys, lift state appropriately.
LibraryPurposeBest For
ReduxCentralized predictable state management (global state)Large apps needing structured data flow
ZustandLightweight state management using hooksSimpler state sharing without boilerplate
React Query (TanStack Query)Server-state management (fetching, caching, syncing)Handling API data efficiently

Example: Redux

// store.ts
import { configureStore, createSlice } from "@reduxjs/toolkit";

const counterSlice = createSlice({
  name: "counter",
  initialState: { count: 0 },
  reducers: { increment: (state) => { state.count++; } }
});

export const { increment } = counterSlice.actions;
export const store = configureStore({ reducer: counterSlice.reducer });

// Counter.tsx
"use client";
import { useDispatch, useSelector } from "react-redux";
import { increment } from "./store";

export default function Counter() {
  const dispatch = useDispatch();
  const count = useSelector((state) => state.count);
  return <button onClick={() => dispatch(increment())}>Count: {count}</button>;
}

Example: Zustand

// useCounterStore.ts
import { create } from "zustand";

export const useCounterStore = create((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
}));

// Component
"use client";
import { useCounterStore } from "./useCounterStore";

export default function Counter() {
  const { count, increment } = useCounterStore();
  return <button onClick={increment}>Zustand Count: {count}</button>;
}

Example: React Query

// app/page.tsx
"use client";
import { useQuery } from "@tanstack/react-query";

export default function Users() {
  const { data, isLoading } = useQuery({
    queryKey: ["users"],
    queryFn: () => fetch("https://jsonplaceholder.typicode.com/users").then(r => r.json()),
  });

  if (isLoading) return <p>Loading...</p>;
  return <ul>{data.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}

Summary:

  • Redux → predictable, structured, global state
  • Zustand → minimal, fast local/global store
  • React Query → asynchronous data fetching & caching
Server data is fetched and stored on load (SSR or ISR), while client data changes locally. To sync:
  1. Use React Query or SWR for automatic refetching
  2. Use mutations and invalidate queries after updates

Example with React Query:

"use client";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";

function Todos() {
  const queryClient = useQueryClient();
  const { data: todos } = useQuery(["todos"], () =>
    fetch("/api/todos").then(r => r.json())
  );

  const addTodo = useMutation({
    mutationFn: (newTodo) =>
      fetch("/api/todos", {
        method: "POST",
        body: JSON.stringify(newTodo),
      }),
    onSuccess: () => queryClient.invalidateQueries(["todos"]),
  });

  return (
    <>
      <ul>{todos?.map(t => <li key={t.id}>{t.title}</li>)}</ul>
      <button onClick={() => addTodo.mutate({ title: "New Task" })}>
        Add Todo
      </button>
    </>
  );
}
Key concept: invalidateQueries ensures fresh data after mutation — syncing server + client.
useReducer is an alternative to useState for managing complex state logic.

Example:

"use client";
import { useReducer } from "react";

const initialState = { count: 0 };

function reducer(state, action) {
  switch (action.type) {
    case "increment": return { count: state.count + 1 };
    case "decrement": return { count: state.count - 1 };
    default: return state;
  }
}

export default function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: "increment" })}>+</button>
      <button onClick={() => dispatch({ type: "decrement" })}>-</button>
    </>
  );
}

When to use:

When state transitions are complex or multiple actions affect the same state.
  • Use API parameters like ?page=1&limit=10
  • Use React Query with getNextPageParam for infinite scroll

Example:

"use client";
import { useInfiniteQuery } from "@tanstack/react-query";

export default function InfiniteUsers() {
  const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({
    queryKey: ["users"],
    queryFn: ({ pageParam = 1 }) =>
      fetch(`https://api.example.com/users?page=${pageParam}`).then(r => r.json()),
    getNextPageParam: (lastPage) => lastPage.nextPage ?? false,
  });

  return (
    <>
      {data?.pages.map((page, i) => (
        <div key={i}>
          {page.users.map((user) => <p key={user.id}>{user.name}</p>)}
        </div>
      ))}
      {hasNextPage && <button onClick={() => fetchNextPage()}>Load more</button>}
    </>
  );
}
Benefit: Efficient, incremental loading instead of fetching the entire dataset.
  • Hydration = The process where React on the client attaches event handlers to the already rendered HTML from the server
  • Hydration mismatch happens when server-rendered HTML differs from client render output

Example (problematic):

// app/page.tsx
"use client";
export default function Page() {
  const now = new Date().toLocaleTimeString();
  return <p>{now}</p>;
}
Issue: Server renders one time, client renders another (time mismatch)

Fix:

"use client";
import { useEffect, useState } from "react";

export default function Page() {
  const [time, setTime] = useState("");
  useEffect(() => setTime(new Date().toLocaleTimeString()), []);
  return <p>{time}</p>;
}

Common causes of mismatch:

  • Conditional rendering differences between server/client
  • Using window, localStorage on the server
  • Non-deterministic values during SSR
Use React Suspense or manual conditional rendering.

Example (React Query):

"use client";
import { useQuery } from "@tanstack/react-query";

function Users() {
  const { data, error, isLoading } = useQuery({
    queryKey: ["users"],
    queryFn: () => fetch("/api/users").then(r => r.json()),
  });

  if (isLoading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;
  return <ul>{data.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
You can persist state using:
  • LocalStorage / SessionStorage
  • URL query params
  • Redux Persist or Zustand middleware

Example (Zustand persist):

import { create } from "zustand";
import { persist } from "zustand/middleware";

export const useUserStore = create(
  persist(
    (set) => ({
      name: "",
      setName: (name) => set({ name }),
    }),
    { name: "user-storage" }
  )
);

Usage:

"use client";
import { useUserStore } from "./useUserStore";

export default function Profile() {
  const { name, setName } = useUserStore();
  return (
    <>
      <input value={name} onChange={(e) => setName(e.target.value)} />
      <p>Hello, {name}</p>
    </>
  );
}
You can use React’s async Server Components to fetch before rendering (SSR-like behavior).

Example:

// app/users/page.tsx
export default async function UsersPage() {
  const users = await fetch("https://api.example.com/users", {
    cache: "no-store", // SSR-like
  }).then(r => r.json());

  return (
    <div>
      {users.map(u => <p key={u.id}>{u.name}</p>)}
    </div>
  );
}

Options:

  • cache: "no-store" → always fresh (SSR)
  • next: { revalidate: 60 } → ISR caching
HOCs are functions that take a component and return a new component with added functionality. They’re used to share logic across multiple components (like authentication, logging, or analytics).

Example:

// withAuth.js
const withAuth = (WrappedComponent) => {
  return function AuthenticatedComponent(props) {
    const isAuthenticated = Boolean(localStorage.getItem("token"));
    if (!isAuthenticated) {
      return <p>Please login first</p>;
    }
    return <WrappedComponent {...props} />;
  };
};

// Usage
const Dashboard = () => <h2>Welcome to Dashboard</h2>;
export default withAuth(Dashboard);
Render Props is a technique where a component accepts a function as a prop and uses it to determine what to render. It helps in sharing logic between components without using HOCs.

Example:

// MouseTracker.js
function MouseTracker({ render }) {
  const [pos, setPos] = React.useState({ x: 0, y: 0 });
  return (
    <div onMouseMove={(e) => setPos({ x: e.clientX, y: e.clientY })}>
      {render(pos)}
    </div>
  );
}

// Usage
<MouseTracker render={({ x, y }) => <p>Mouse at {x}, {y}</p>} />
Compound components allow multiple components to work together, sharing implicit state via React Context.

Example:

const TabsContext = React.createContext();

function Tabs({ children }) {
  const [active, setActive] = React.useState(0);
  return (
    <TabsContext.Provider value={{ active, setActive }}>
      <div>{children}</div>
    </TabsContext.Provider>
  );
}

function TabList({ children }) {
  return <div>{children}</div>;
}

function Tab({ index, children }) {
  const { active, setActive } = React.useContext(TabsContext);
  return (
    <button
      onClick={() => setActive(index)}
      style={{ fontWeight: active === index ? "bold" : "normal" }}
    >
      {children}
    </button>
  );
}

function TabPanels({ children }) {
  const { active } = React.useContext(TabsContext);
  return <div>{children[active]}</div>;
}

// Usage
<Tabs>
  <TabList>
    <Tab index={0}>Profile</Tab>
    <Tab index={1}>Settings</Tab>
  </TabList>
  <TabPanels>
    <div>Profile content</div>
    <div>Settings content</div>
  </TabPanels>
</Tabs>
The Provider Pattern exposes shared data and behavior to components via React Context. It’s heavily used for themes, authentication, and state management.

Example:

const ThemeContext = React.createContext();

export function ThemeProvider({ children }) {
  const [theme, setTheme] = React.useState("light");
  const toggle = () => setTheme(t => (t === "light" ? "dark" : "light"));

  return (
    <ThemeContext.Provider value={{ theme, toggle }}>
      {children}
    </ThemeContext.Provider>
  );
}

// Usage
function ThemeToggleButton() {
  const { theme, toggle } = React.useContext(ThemeContext);
  return <button onClick={toggle}>Theme: {theme}</button>;
}
Code-splitting helps load only the code needed for the current page, improving performance. React provides React.lazy() and Suspense for this.

Example:

const Dashboard = React.lazy(() => import("./Dashboard"));

function App() {
  return (
    <React.Suspense fallback={<p>Loading...</p>}>
      <Dashboard />
    </React.Suspense>
  );
}
In Next.js, code-splitting is automatic per page.
A common scalable structure:
src/
 ├── app/
 │    ├── (routes)
 │    ├── layout.tsx
 │    └── page.tsx
 ├── components/
 │    ├── ui/
 │    ├── forms/
 │    ├── layout/
 ├── hooks/
 ├── context/
 ├── lib/
 ├── services/
 ├── store/
 ├── styles/
 └── utils/
  • Feature-based organization helps scale
  • Shared state or UI logic goes to /context or /hooks
  • Pages are lazy-loaded in Next.js automatically
Error boundaries catch JavaScript errors in React components and show fallback UIs instead of breaking the app. They only work in class components.

Example:

class ErrorBoundary extends React.Component {
  state = { hasError: false };
  static getDerivedStateFromError() {
    return { hasError: true };
  }
  componentDidCatch(error, info) {
    console.error("Error logged:", error, info);
  }
  render() {
    if (this.state.hasError) return <h2>Something went wrong!</h2>;
    return this.props.children;
  }
}

// Usage
<ErrorBoundary>
  <ComponentThatMayCrash />
</ErrorBoundary>
To prevent unnecessary re-renders:
  • Use React.memo() for pure functional components
  • Use useCallback and useMemo to memoize functions and computed values
  • Avoid creating new objects/functions in render unnecessarily

Example:

const Button = React.memo(({ onClick }) => {
  console.log("Rendered Button");
  return <button onClick={onClick}>Click me</button>;
});

function App() {
  const handleClick = React.useCallback(() => console.log("Clicked!"), []);
  return <Button onClick={handleClick} />;
}
AspectMonolithicModular (Micro-frontend)
StructureAll components tightly coupled and deployed togetherApp divided into independent modules (built & deployed separately)
Team SizeBetter for small teamsIdeal for large teams, multiple domains
DeploymentSingle deploymentIndependent deployments
Tech StackSingle technologyTech diversity (React + Vue + Angular)

When to use Modular:

  • Large teams, multiple domains
  • Independent deployments
  • Tech diversity requirements
Options:
  • Context API for light global state
  • Redux Toolkit / Zustand / Jotai for complex or shared logic
  • React Query for server state

Example with Zustand:

import { create } from "zustand";

const useStore = create((set) => ({
  count: 0,
  increase: () => set((state) => ({ count: state.count + 1 })),
}));

function Counter() {
  const { count, increase } = useStore();
  return <button onClick={increase}>Count: {count}</button>;
}

8. React and Next JS testing

Testing ensures that UI and logic behave as expected after changes. It helps catch regressions early and builds confidence in refactoring.

Types of tests:

  • Unit tests → Test small pieces (functions, components)
  • Integration tests → Verify component interaction
  • E2E tests → Test entire user flows (via Playwright / Cypress)
  • Jest → Testing framework (built-in with Next.js)
  • React Testing Library (RTL) → DOM interaction & behavior testing
  • Playwright / Cypress → End-to-end browser testing

Example Component:

// Counter.js
export default function Counter() {
  const [count, setCount] = React.useState(0);
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount((c) => c + 1)}>Increment</button>
    </div>
  );
}

Test File:

// Counter.test.js
import { render, screen, fireEvent } from "@testing-library/react";
import Counter from "./Counter";

test("increments counter when button clicked", () => {
  render(<Counter />);
  fireEvent.click(screen.getByText("Increment"));
  expect(screen.getByText("Count: 1")).toBeInTheDocument();
});
Key Concept: RTL focuses on testing how users interact, not implementation details.
You can use Jest’s jest.fn() or libraries like MSW (Mock Service Worker).

Example using Jest mock:

// fetchUser.js
export const fetchUser = async () => {
  const res = await fetch("/api/user");
  return res.json();
};

// fetchUser.test.js
import { fetchUser } from "./fetchUser";

global.fetch = jest.fn(() =>
  Promise.resolve({ json: () => Promise.resolve({ name: "Shehroze" }) })
);

test("fetches user correctly", async () => {
  const user = await fetchUser();
  expect(user.name).toBe("Shehroze");
});
You can test server actions, API routes, or getServerSideProps using Jest’s mocks.

Example:

// app/api/hello/route.js
export async function GET() {
  return Response.json({ message: "Hello Next.js" });
}

// route.test.js
import { GET } from "./route";

test("returns hello message", async () => {
  const response = await GET();
  const data = await response.json();
  expect(data.message).toBe("Hello Next.js");
});
Snapshot tests ensure UI doesn’t change unexpectedly.

Example:

import { render } from "@testing-library/react";
import Button from "./Button";

test("renders correctly", () => {
  const { asFragment } = render(<Button label="Click me" />);
  expect(asFragment()).toMatchSnapshot();
});
If the UI changes, Jest will show a diff.
Wrap your component inside the provider in the test.

Example:

// ThemeContext.js
const ThemeContext = React.createContext();
export const ThemeProvider = ({ children }) => (
  <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
);
export default ThemeContext;

// ThemedText.js
import ThemeContext from "./ThemeContext";
export default function ThemedText() {
  const theme = React.useContext(ThemeContext);
  return <p>Theme: {theme}</p>;
}

// ThemedText.test.js
import { render, screen } from "@testing-library/react";
import { ThemeProvider } from "./ThemeContext";
import ThemedText from "./ThemedText";

test("renders theme from context", () => {
  render(
    <ThemeProvider>
      <ThemedText />
    </ThemeProvider>
  );
  expect(screen.getByText("Theme: dark")).toBeInTheDocument();
});
  • Use React Developer Tools in browser
  • Add console.log() inside hooks or effects
  • Use VSCode breakpoints
  • Use why-did-you-render library to detect unnecessary re-renders
  • For Next.js, run next dev --inspect for Node debugging
Jest has built-in coverage reporting:
npm run test -- --coverage
It generates a report showing what percentage of lines, functions, and branches are tested.

Key metrics to aim for:

  • 80%+ line coverage
  • 70%+ branch coverage
ProblemPossible CauseSolution
Hydration ErrorMismatch between SSR and client renderingAvoid using browser-only APIs during SSR
API Route not workingWrong file structure or method nameEnsure correct /app/api/.../route.js naming
404 after deployStatic export paths missingCheck dynamic routes and revalidate configs
Performance lagOver-fetching or large bundleUse lazy loading, memoization, code-splitting

Example Component:

// UserProfile.js
export default function UserProfile() {
  const [user, setUser] = React.useState(null);
  React.useEffect(() => {
    fetch("/api/user")
      .then((res) => res.json())
      .then(setUser);
  }, []);
  if (!user) return <p>Loading...</p>;
  return <p>Hello, {user.name}</p>;
}

Test File:

// UserProfile.test.js
import { render, screen, waitFor } from "@testing-library/react";
import UserProfile from "./UserProfile";

global.fetch = jest.fn(() =>
  Promise.resolve({ json: () => Promise.resolve({ name: "Shehroze" }) })
);

test("renders user after fetch", async () => {
  render(<UserProfile />);
  await waitFor(() => expect(screen.getByText("Hello, Shehroze")).toBeInTheDocument());
});

9. Performance and Optimization React | Next JS

To prevent unnecessary re-renders in React:
  • Wrap pure components with React.memo
  • Use useCallback to memoize event handlers
  • Use useMemo for computed values
  • Split large components
  • Keep state as local as possible

Example:

const Child = React.memo(({ value }) => {
  console.log("Child rendered");
  return <p>{value}</p>;
});

function App() {
  const [count, setCount] = React.useState(0);
  const memoValue = React.useMemo(() => count * 2, [count]);
  return (
    <>
      <Child value={memoValue} />
      <button onClick={() => setCount((c) => c + 1)}>Increment</button>
    </>
  );
}
Use the built-in next/image component for automatic:
  • Lazy loading
  • Responsive sizes
  • Format optimization (WebP/AVIF)

Example:

import Image from "next/image";

export default function Hero() {
  return (
    <Image
      src="/hero.jpg"
      alt="Hero Image"
      width={1200}
      height={600}
      priority
    />
  );
}

Tips:

  • Use priority for above-the-fold images
  • Use blurDataURL for placeholder effect
  • Use dynamic imports:
const Chart = dynamic(() => import("../components/Chart"), { ssr: false });
  • Analyze with:
npm run build && npx next analyze
  • Move heavy logic to server components (Next.js App Router)
Suspense lets you pause rendering while data is loading, improving perceived performance.

Example:

const User = React.lazy(() => import("./User"));

function App() {
  return (
    <React.Suspense fallback={<p>Loading user...</p>}>
      <User />
    </React.Suspense>
  );
}
  • Server Components: Run on the server; ideal for data fetching and heavy logic
  • Client Components: Run in the browser; used for interactivity

Example:

// server component (default)
export default async function Page() {
  const data = await fetch("https://api.example.com/posts").then(r => r.json());
  return <List posts={data} />;
}

// client component
"use client";
export function List({ posts }) {
  return posts.map(p => <p key={p.id}>{p.title}</p>);
}
  • Use revalidate for ISR (Incremental Static Regeneration)
  • Use fetch caching options (force-cache, no-store, revalidate)

Example:

export default async function Page() {
  const res = await fetch("https://api.example.com/data", { 
    next: { revalidate: 60 } 
  });
  const data = await res.json();
  return <div>{data.title}</div>;
}
  • React Profiler → Measures render time
  • Lighthouse → Measures Core Web Vitals
  • Next.js build analyzer → Bundle size breakdown
  • Chrome DevTools Performance tab → JS execution time
React rendering has:
  1. Render phase → Reconciliation (diffing virtual DOM)
  2. Commit phase → Apply changes to real DOM
Use React DevTools Profiler to visualize and measure these phases.
  • Use windowing (e.g. react-window or react-virtualized)
  • Use key properly
  • Paginate data

Example:

import { FixedSizeList as List } from "react-window";

const Row = ({ index, style }) => <div style={style}>Row {index}</div>;

<List height={400} width={300} itemCount={1000} itemSize={35}>
  {Row}
</List>;
  • Use generateMetadata() in App Router
  • Lazy load below-the-fold components
  • Preload fonts via next/font
  • Optimize routes with static rendering when possible
  • Use semantic HTML and proper heading structure
  • Implement structured data with JSON-LD
  • Ensure fast loading times with image optimization

10. Security and Best Practices in React | Next JS

React escapes all strings by default, preventing XSS unless you use dangerouslySetInnerHTML.

Safe Practice:

Never insert user input into HTML directly.
<p>{userInput}</p> // ✅ Safe
<div dangerouslySetInnerHTML={{ __html: userInput }} /> // ❌ Dangerous
If you must, sanitize it using DOMPurify:
import DOMPurify from "dompurify";
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userInput) }} />;
  • Use authentication middleware
  • Validate incoming data with Zod or Yup
  • Restrict methods (GET, POST, etc.)
  • Avoid exposing sensitive env vars to the client

Example:

// app/api/user/route.js
import { auth } from "@/lib/auth";
import { z } from "zod";

const schema = z.object({ name: z.string() });

export async function POST(req) {
  const user = await auth(req);
  if (!user) return new Response("Unauthorized", { status: 401 });

  const body = await req.json();
  schema.parse(body);
  // safe to use body.name
}
  • Use .env.local for local secrets
  • Access via process.env.SECRET_KEY on server
  • Never expose private keys using NEXT_PUBLIC_ prefix unless intentional
Environment variables without NEXT_PUBLIC_ prefix are only available on the server side.
  • Use tokens or double-submit cookies
  • For sensitive actions, validate Origin and Referer headers
  • Use libraries like NextAuth.js (handles CSRF internally)
  • Implement SameSite cookies
cookies.set("token", jwt, { 
  httpOnly: true, 
  secure: true, 
  sameSite: "strict" 
});
  • Use HTTP-only cookies for tokens
  • Avoid storing JWT in localStorage (can be stolen via XSS)
  • Implement proper session management
  • Use secure and sameSite flags for cookies
cookies.set("token", jwt, { 
  httpOnly: true, 
  secure: true, 
  sameSite: "strict" 
});
HTTP-only cookies prevent JavaScript access, reducing XSS attack impact.
A CSP header restricts sources for scripts, styles, images, etc., reducing XSS risk.

Example:

// next.config.js
async headers() {
  return [
    {
      source: "/(.*)",
      headers: [
        {
          key: "Content-Security-Policy",
          value: "default-src 'self'; img-src https: data:; script-src 'self';",
        },
      ],
    },
  ];
}
CSP helps prevent XSS attacks by whitelisting trusted sources for content loading.
Use middleware like Upstash, Redis, or rate-limiter-flexible.

Example (middleware):

import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";

const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.fixedWindow(10, "60 s"),
});

export async function middleware(req) {
  const ip = req.ip ?? "127.0.0.1";
  const { success } = await ratelimit.limit(ip);
  if (!success) return new Response("Too many requests", { status: 429 });
  return NextResponse.next();
}
Never log JWTs, passwords, or full API responses. Mask sensitive data before logging.

Example:

// ❌ Don't do this
console.log({ email, password: userPassword });

// ✅ Do this instead
console.log({ email, password: "***" });

// For debugging, use redaction
const safeLog = (data) => {
  const { password, token, ...safeData } = data;
  console.log({ ...safeData, sensitive: "REDACTED" });
};
Always redact sensitive information before logging to prevent data leaks.
  • Always whitelist origins
  • Avoid using * in production
  • Use middleware to configure headers

Example:

export const config = { api: { bodyParser: false } };
export default function handler(req, res) {
  res.setHeader("Access-Control-Allow-Origin", "https://trusted.com");
  res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT");
  res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
}
Never use Access-Control-Allow-Origin: * in production for sensitive endpoints.

Security Checklist:

  • ✅ Use HTTPS
  • ✅ Set NODE_ENV=production
  • ✅ Use environment variables, not hardcoded secrets
  • ✅ Keep dependencies updated (npm audit)
  • ✅ Use a WAF (Web Application Firewall) if available
  • ✅ Implement proper CORS policies
  • ✅ Use security headers (CSP, HSTS, X-Frame-Options)
  • ✅ Regular security scanning and penetration testing
  • ✅ Monitor for suspicious activities
  • ✅ Implement proper error handling (don’t leak stack traces)
Regular security audits and dependency updates are crucial for maintaining application security.
Environment variables starting with NEXT_PUBLIC_ are exposed to the client. Sensitive keys (like API secrets or DB credentials) must not use NEXT_PUBLIC_ and should only be accessed on the server (API routes, getServerSideProps, or server components).

Example:

# .env.local
NEXT_PUBLIC_API_URL=https://api.example.com
SECRET_API_KEY=my_super_secret_key
// app/api/data/route.js
export async function GET() {
  const res = await fetch("https://secureapi.com", {
    headers: { Authorization: `Bearer ${process.env.SECRET_API_KEY}` },
  });
  const data = await res.json();
  return Response.json(data);
}
Secret key never goes to the browser when not using NEXT_PUBLIC_ prefix.

1️⃣ XSS (Cross-Site Scripting):

Occurs when malicious scripts are injected into the DOM.✅ Fix:
  • Avoid dangerouslySetInnerHTML
  • Sanitize input using libraries like DOMPurify
import DOMPurify from "dompurify";

function SafeHtml({ html }) {
  return <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(html) }} />;
}

2️⃣ CSRF (Cross-Site Request Forgery):

Attackers trick users into making unwanted requests.✅ Fix:
  • Use anti-CSRF tokens (NextAuth and other libs handle this)
  • Validate origin headers in API routes

3️⃣ Data Leakage via Source Code:

Leaking private tokens in frontend code.✅ Fix:
  • Never expose secrets in NEXT_PUBLIC_ vars
  • Validate .env usage during CI/CD

4️⃣ Insecure Direct Object Reference (IDOR):

Users access others’ data by manipulating IDs.✅ Fix:
  • Always verify authorization on the server
  • Never trust frontend route params

Approach 1: Using NextAuth.js

// app/api/auth/[...nextauth]/route.js
import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";

const handler = NextAuth({
  providers: [
    CredentialsProvider({
      async authorize(credentials) {
        const res = await fetch("https://api.example.com/login", {
          method: "POST",
          body: JSON.stringify(credentials),
        });
        const user = await res.json();
        if (user?.token) return user;
        return null;
      },
    }),
  ],
  session: { strategy: "jwt" },
});

export { handler as GET, handler as POST };
Uses server-side token validation. Sessions stored as HTTP-only cookies (protects from XSS).
  • HttpOnly flag prevents JavaScript from reading cookies
  • Reduces XSS impact since scripts can’t access sensitive tokens

Example:

// Setting cookie securely
cookies().set("token", jwt, { 
  httpOnly: true, 
  secure: true, 
  sameSite: "Strict" 
});
Prevents token theft from browser scripts by making cookies inaccessible to JavaScript.
CORS controls which domains can make API requests.

Example:

// app/api/route.js
export async function GET(req) {
  return new Response("ok", {
    headers: {
      "Access-Control-Allow-Origin": "https://yourdomain.com",
      "Access-Control-Allow-Credentials": "true",
    },
  });
}
Never use ”*” for production as it allows any domain to access your API.
Attackers could redirect users to malicious sites.✅ Fix: Validate redirect URLs before allowing them:
export async function POST(req) {
  const { redirectUrl } = await req.json();
  const safeDomains = ["example.com", "app.example.com"];
  const url = new URL(redirectUrl);

  if (!safeDomains.includes(url.hostname))
    return new Response("Invalid redirect", { status: 400 });

  return Response.redirect(redirectUrl);
}

Example:

import { cookies } from "next/headers";

export async function GET() {
  const token = cookies().get("token");
  if (!token) return Response.json({ error: "Unauthorized" }, { status: 401 });
  
  // verify JWT, then fetch data
  const user = await verifyToken(token.value);
  return Response.json({ data: user });
}
Authentication handled on the server, not the client.
CSP prevents malicious scripts or resources from loading.

Example:

// next.config.js
const securityHeaders = [
  {
    key: "Content-Security-Policy",
    value: "default-src 'self'; img-src *; script-src 'self'; style-src 'self';",
  },
];

module.exports = {
  async headers() {
    return [{ source: "/(.*)", headers: securityHeaders }];
  },
};
Helps prevent XSS and data injection attacks by restricting content sources.

Example:

import jwt from "jsonwebtoken";
import { cookies } from "next/headers";

export async function GET() {
  const token = cookies().get("token")?.value;
  if (!token) return Response.json({ message: "No token" }, { status: 401 });

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    return Response.json({ user: decoded });
  } catch {
    return Response.json({ message: "Invalid token" }, { status: 403 });
  }
}
Validates access at runtime using JWT verification.

Example:

"use server";
import { cookies } from "next/headers";

export async function deleteUserAccount() {
  const token = cookies().get("token");
  if (!token) throw new Error("Unauthorized");

  // call backend securely
  await deleteAccountFromDatabase();
}
Server actions run on the server only, so secrets remain safe.
CategoryBest Practice
🔐 AuthenticationUse JWT or NextAuth with HttpOnly cookies
🔑 SecretsStore in .env.local, never in client
🧱 XSSSanitize HTML, avoid dangerouslySetInnerHTML
🔄 CSRFUse anti-CSRF tokens or same-site cookies
🧭 RoutingValidate redirect URLs
🧰 PackagesAudit dependencies with npm audit
⚙️ HeadersSet CSP, X-Frame-Options, Strict-Transport-Security
📦 APIRate limit requests and validate input
📡 HTTPSAlways use SSL in production
Regular security audits and dependency updates are essential for maintaining application security.

11. Node.js Advanced Topics

Node.js is a JavaScript runtime built on V8 engine, enabling server-side JavaScript. Features: event-driven, non-blocking I/O, single-threaded event loop, NPM ecosystem.
Node.js is single-threaded, but it handles concurrency using the event loop, which is part of the libuv library. The event loop allows Node.js to perform non-blocking I/O operations, even though JavaScript runs on a single thread.

Event Loop Phases (TCCPPI)

  1. Timers – Executes callbacks from setTimeout() and setInterval()
  2. Pending Callbacks – Executes I/O callbacks deferred from the previous cycle
  3. Idle / Prepare – Internal use
  4. Poll – Retrieves new I/O events; executes their callbacks
  5. Check – Executes callbacks from setImmediate()
  6. Close Callbacks – Executes callbacks like socket.on('close')

Example

setTimeout(() => console.log("Timeout"), 0);
setImmediate(() => console.log("Immediate"));
process.nextTick(() => console.log("NextTick"));
console.log("Sync");
Output:
Sync
NextTick
Timeout
Immediate

Why this order?

  • process.nextTick() runs before the event loop continues
  • Timers (setTimeout) are queued for the Timers phase
  • setImmediate() runs during the Check phase

When to use:

  • Use process.nextTick() for micro-tasks after the current operation
  • Use setImmediate() for tasks after I/O events are processed
Both schedule asynchronous callbacks, but they run at different times in the event loop.
  • process.nextTick() executes before the event loop continues — a micro-task
  • setImmediate() executes after the current poll phase — a macro-task

Example:

console.log('Start');
process.nextTick(() => console.log('Next Tick'));
setImmediate(() => console.log('Immediate'));
console.log('End');
Output:
Start
End
Next Tick
Immediate

When to use:

  • Use process.nextTick() for quick callbacks that must run before I/O
  • Use setImmediate() when you want to yield to I/O first to prevent blocking
Node.js uses libuv, a C library providing an event-driven, non-blocking I/O model. I/O operations (file read, network calls, etc.) are delegated to the OS kernel or libuv’s thread pool, so Node’s main thread stays free to handle other tasks.

Example:

const fs = require('fs');

console.log('Start');
fs.readFile('file.txt', 'utf8', (err, data) => {
  if (err) throw err;
  console.log('File read complete');
});
console.log('End');
Output:
Start
End
File read complete

Why:

The file read happens asynchronously; Node doesn’t block waiting for it.

Where/When:

Used in high-throughput systems — e.g., API gateways, chat apps — where many concurrent requests are served without thread blocking.
Streams are continuous data flows — they let you process data chunk by chunk instead of loading it all into memory. They’re instances of the EventEmitter class.

Types of Streams:

  1. Readable – e.g. fs.createReadStream()
  2. Writable – e.g. fs.createWriteStream()
  3. Duplex – both readable and writable (e.g. TCP socket)
  4. Transform – modifies data while reading/writing (e.g. zlib compression)

Example:

const fs = require('fs');
const read = fs.createReadStream('input.txt');
const write = fs.createWriteStream('output.txt');

read.pipe(write);

Why:

  • Efficient for large files or live data (video, logs)
  • Uses constant memory regardless of file size

Where:

  • File uploads/downloads
  • Real-time data transfer
  • Log streaming
MethodDescriptionUse Case
spawnLaunches a new processFor long-running or streaming output
execLaunches a process and buffers entire outputFor small output commands
forkSpecial case of spawn that runs a Node.js script with IPCFor creating worker processes

Example:

const { spawn, exec, fork } = require('child_process');

// spawn example
const ls = spawn('ls', ['-lh', '/usr']);
ls.stdout.on('data', data => console.log(`Spawn output: ${data}`));

// exec example
exec('ls -lh /usr', (err, stdout) => console.log(`Exec output: ${stdout}`));

// fork example
const child = fork('./worker.js');
child.on('message', msg => console.log('Message from child:', msg));

Why/When/Where:

  • Use spawn for streaming output (e.g., logs)
  • Use exec when you need full command output as a string
  • Use fork for scaling CPU-bound tasks or worker threads
Worker Threads allow multi-threading in Node.js for CPU-intensive tasks, unlike the single-threaded event loop.

Example:

const { Worker, isMainThread, parentPort } = require('worker_threads');

if (isMainThread) {
  const worker = new Worker(__filename);
  worker.on('message', msg => console.log('Received:', msg));
} else {
  parentPort.postMessage('Hello from Worker');
}

Why:

To offload heavy computations (e.g. image processing, encryption) so they don’t block the main event loop.

Where:

In apps with mixed I/O and CPU workloads, like video encoding servers.
You should always handle unexpected errors globally, but never rely solely on them — they indicate bugs that should be fixed.

Example:

process.on('uncaughtException', (err) => {
  console.error('Uncaught Exception:', err);
  process.exit(1); // Restart service safely
});

process.on('unhandledRejection', (reason) => {
  console.error('Unhandled Promise Rejection:', reason);
});

Why:

Prevents app from crashing unexpectedly and allows logging/restarting.

When/Where:

Use this for graceful shutdown and to catch programming mistakes in production.
Cluster mode allows you to spawn multiple Node.js processes (workers) to utilize multi-core CPUs. Each worker runs a separate instance of your app and shares the same server port via IPC managed by the cluster module.

Example:

const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;

if (cluster.isPrimary) {
  for (let i = 0; i < numCPUs; i++) cluster.fork();
  cluster.on('exit', (worker) => cluster.fork());
} else {
  http.createServer((req, res) => {
    res.end(`Handled by worker ${process.pid}`);
  }).listen(3000);
}

Why:

  • Single Node.js process uses only one CPU core
  • Cluster mode scales horizontally across all cores

Where:

Used in production APIs — e.g., Express servers, GraphQL APIs — to handle more traffic efficiently.
Node.js uses V8’s garbage collector, which manages heap memory automatically, but developers must avoid leaks.

Common causes of leaks:

  • Global variables
  • Unclosed timers or listeners
  • Caching large data objects indefinitely

Example Leak:

const cache = {};
setInterval(() => {
  cache[Math.random()] = new Array(1000000).join('x');
}, 1000);
Fix: Use WeakMap, clear intervals, or implement LRU caches.

When/Where:

Monitor memory with tools like:
  • --inspect + Chrome DevTools
  • clinic.js, heapdump, node --trace-gc
Buffers are binary data containers — used to handle raw data (files, streams, sockets) that can’t be represented as strings.

Example:

const buf = Buffer.from('Hello');
console.log(buf); // <Buffer 48 65 6c 6c 6f>
console.log(buf.toString()); // 'Hello'

Why:

Buffers allow manipulation of binary data efficiently (e.g., file systems, TCP streams).

Where:

Used in file operations, network protocols, and binary serialization.
Node.js uses CommonJS (require) and ES Modules (import) systems.

Example:

// math.js
module.exports.add = (a, b) => a + b;

// app.js
const { add } = require('./math');
console.log(add(2, 3));

Module Resolution Order:

  1. Core modules (fs, path)
  2. Local files (./, ../)
  3. node_modules directory

Why:

Encapsulation of code — prevents global scope pollution.

Where:

Used in all Node.js apps; ES Modules preferred for modern codebases.
Large Node.js applications need modular, layered architecture to keep code organized and maintainable.

Common Structure:

src/
 ┣ config/          # Environment variables, constants
 ┣ modules/
 ┃ ┣ user/
 ┃ ┃ ┣ user.controller.js
 ┃ ┃ ┣ user.service.js
 ┃ ┃ ┣ user.model.js
 ┃ ┃ ┣ user.routes.js
 ┣ middlewares/
 ┣ utils/
 ┣ app.js
 ┣ server.js

Layers Explained:

  • Controller: Handles request/response (HTTP logic)
  • Service: Business logic
  • Model: Database schema and queries
  • Middleware: Reusable pre-processing (auth, validation)
  • Routes: Maps endpoints to controllers

Why:

  • Improves separation of concerns
  • Enables unit testing per layer
  • Simplifies onboarding and scalability

Where/When:

Used in all enterprise-level Node.js APIs or microservices where team collaboration and modularization are essential.
Node.js heavily uses asynchronous and modular design patterns.

Key Patterns:

PatternDescriptionExampleUse Case
SingletonSingle shared instanceDB connection poolDatabase connections
FactoryCreates objects dynamicallyModel creationDynamic service instantiation
ObserverEvent-drivenEventEmitterReal-time systems
MiddlewareChain of functionsExpress middlewaresAPI requests
RepositoryAbstract data layerRepository classDecoupled DB logic
DecoratorAdds behavior without alteringWrapping servicesLogging, caching

Singleton Pattern Example:

class Database {
  constructor() {
    if (Database.instance) return Database.instance;
    this.connection = this.connect();
    Database.instance = this;
  }
  connect() {
    console.log("DB connected");
    return {};
  }
}
const db1 = new Database();
const db2 = new Database();
console.log(db1 === db2); // true
Why: Ensures only one DB connection exists across the app
When: Use for connection pools, config objects, caches

Factory Pattern Example:

class Payment {
  process() {}
}

class Paypal extends Payment {
  process() { console.log("PayPal Payment"); }
}

class Stripe extends Payment {
  process() { console.log("Stripe Payment"); }
}

class PaymentFactory {
  static create(type) {
    if (type === "paypal") return new Paypal();
    if (type === "stripe") return new Stripe();
  }
}

const payment = PaymentFactory.create("paypal");
payment.process(); // PayPal Payment
Why: Avoids tight coupling between code and object types
When: Use for service selection (e.g., multiple gateways, APIs)
MVC separates your application into:
  • Model: Manages data and database operations
  • View: Renders UI (in REST APIs, often JSON)
  • Controller: Handles requests, uses Model to get data, and sends responses

Example:

// user.model.js
export const User = mongoose.model('User', new Schema({ name: String }));

// user.controller.js
import { User } from './user.model.js';
export const getUsers = async (req, res) => {
  const users = await User.find();
  res.json(users);
};

// user.routes.js
router.get('/users', getUsers);

Why:

Encourages separation of concerns, clean testing, and reusability.

Where/When:

Common in Express-based web APIs and server-rendered apps.
Microservices = small, independent services communicating via APIs or message queues.

Key Components:

  • Each service has own database, own deployment
  • Communication via REST, gRPC, or message queues (e.g., RabbitMQ, Kafka)
  • Use API Gateway for centralized routing/auth

Example Setup:

user-service      → handles users
order-service     → handles orders
payment-service   → handles payments
api-gateway       → routes and aggregates requests

Example Communication:

// user-service calls order-service via REST
const axios = require('axios');
const orders = await axios.get(`http://order-service/orders?userId=${userId}`);

Why:

  • Independent scaling and deployment
  • Easier fault isolation
  • Better for large teams or multi-domain systems

When/Where:

Used in enterprise systems (e.g., eCommerce, SaaS) where modularity and scaling are critical.
Node.js microservices can communicate via:
TypeExampleUse Case
Synchronous (Request-Response)REST, gRPCReal-time data
Asynchronous (Event-driven)RabbitMQ, Kafka, Redis Pub/SubDecoupled systems

Asynchronous Messaging Example (RabbitMQ):

const amqp = require('amqplib');

(async () => {
  const conn = await amqp.connect('amqp://localhost');
  const channel = await conn.createChannel();
  await channel.assertQueue('orderQueue');
  channel.sendToQueue('orderQueue', Buffer.from('New order created'));
})();

Why:

Async messaging improves resilience and fault tolerance.

When/Where:

Use async communication when loose coupling is desired (e.g., sending notifications after order creation).
Repository Pattern abstracts database logic into a single layer, separating it from business logic.

Example:

// user.repository.js
export class UserRepository {
  async findByEmail(email) {
    return User.findOne({ email });
  }
}

// user.service.js
const repo = new UserRepository();
const user = await repo.findByEmail(req.body.email);

Why:

  • Makes business logic database-agnostic
  • Simplifies unit testing (mock repositories)
  • Promotes clean architecture

When/Where:

Used in large teams where database changes should not affect business logic.
Dependency Injection (DI) means passing dependencies (services, repositories, etc.) into classes/functions instead of creating them inside.

Example:

class EmailService {
  send(email, msg) { console.log(`Email sent to ${email}`); }
}

class UserController {
  constructor(emailService) {
    this.emailService = emailService;
  }
  register(user) {
    this.emailService.send(user.email, "Welcome!");
  }
}

const emailService = new EmailService();
const userController = new UserController(emailService);

Why:

  • Enables loose coupling and easier testing
  • Supports inversion of control

When/Where:

Used in testable architectures, e.g., NestJS framework (which has DI built-in).
Store configurations separately for each environment (dev, staging, prod).

Example Structure:

config/
 ┣ default.json
 ┣ development.json
 ┣ production.json
Use libraries like dotenv or config.

Example with dotenv:

require('dotenv').config();
console.log(process.env.DB_HOST);

Why:

  • Keeps secrets out of source code
  • Easier environment portability

Where/When:

In CI/CD pipelines and multi-environment deployments (e.g., AWS, Docker).
Layered architecture separates the backend into logical layers — each with its responsibility.

Typical Layers:

  1. Presentation (Controller) – Handles requests
  2. Business (Service) – Contains logic
  3. Data (Repository) – Handles persistence
  4. Integration (External APIs) – Handles external comms

Example Flow:

Controller → Service → Repository → Database

Why:

  • Makes the system modular, testable, and maintainable

Where:

Used in enterprise backends and API-first architectures.
Versioning allows you to introduce new features without breaking old clients.

Ways to Version:

  1. URL-based: /api/v1/users
  2. Header-based: Accept: application/vnd.api.v2+json
  3. Query-based: /users?version=2

Example:

app.use('/api/v1', v1Routes);
app.use('/api/v2', v2Routes);

Why:

Maintains backward compatibility and smooth client migration.

When/Where:

Use when rolling out breaking API changes.
CQRS separates read and write operations into different models/services.

Example:

// Command (write)
POST /orderscreates order in DB

// Query (read)
GET /orders/:idreads from optimized read model or cache

Why:

  • Improves scalability (writes and reads can scale independently)
  • Enables event sourcing and audit trails

Where/When:

Used in financial, e-commerce, and high-scale event systems.
Modules in Node.js are reusable blocks of code that encapsulate functionality. They help in organizing code into manageable, independent components.

Node.js supports three main types of modules:

  1. Core Modules (built-in) → e.g., fs, path, http
  2. Local Modules → custom files in your app
  3. Third-party Modules → installed from npm (e.g., express, lodash)

Example:

// math.js (Local module)
function add(a, b) {
  return a + b;
}
module.exports = { add };

// app.js
const { add } = require('./math');
console.log(add(5, 10));

Why:

To promote reusability and separation of concerns. Without modules, everything would live in one file, making maintenance and testing hard.

When:

Use modules when splitting logic — routes, services, utilities, models, etc.

Where:

Common in every medium to large-scale Node.js app, especially with MVC or layered architecture.
FeatureCommonJS (require)ES Modules (import)
LoadingSynchronousAsynchronous
Syntaxconst x = require('x')import x from 'x'
ScopeWrapped in functionStrict top-level
Default inNode.js before v14Modern Node.js (with “type”: “module”)

Example:

// CommonJS
const express = require('express');

// ES Module
import express from 'express';

Why:

ESM is the modern standard (tree-shaking, async loading, static analysis).

When:

  • Use CommonJS in legacy or mixed codebases
  • Use ESM for modern projects (especially with TypeScript or Next.js APIs)

Where:

Configured via package.json → "type": "module"
ConceptMVCLayered Architecture
PatternModel–View–ControllerRequest–Controller–Service–Repository
PurposeWeb apps with UIAPIs and backend systems
FocusSeparation of UI and logicLogical separation by responsibility

Example Layered Architecture Flow:

Request → Controller → Service → Repository → Database
// userController.js
const userService = require('../services/userService');
exports.getUser = async (req, res) => {
  const user = await userService.getById(req.params.id);
  res.json(user);
};

// userService.js
const userRepo = require('../repositories/userRepo');
exports.getById = async (id) => await userRepo.findById(id);

Why:

Each layer has a single responsibility — easy to test and change independently.

When:

For scalable APIs and microservices.

Where:

Used in enterprise Node.js apps (Express, NestJS, Fastify-based systems).
Using dotenv or process.env to manage secrets and configurations per environment (dev, staging, production).

Example:

# .env file
PORT=5000
DB_URI=mongodb://localhost:27017/mydb
// config.js
require('dotenv').config();
module.exports = {
  port: process.env.PORT,
  db: process.env.DB_URI,
};

Why:

Keeps sensitive data (like DB passwords, API keys) out of code.

When:

Always use environment variables for configuration.

Where:

  • process.env → globally accessible
  • Used in CI/CD pipelines, Docker, Kubernetes secrets, etc.
Dependency Injection (DI) is a design pattern where dependencies are injected into a module instead of being hardcoded inside it.

Example:

// Without DI:
const userRepo = require('./userRepo');
exports.getUser = () => userRepo.findAll();

// With DI:
module.exports = (userRepo) => ({
  getUser: () => userRepo.findAll(),
});

Why:

  • Makes testing easier (can inject mocks)
  • Improves flexibility and maintainability

When:

Used heavily in frameworks like NestJS or in test-driven architectures.

Where:

Service layers, repositories, utilities, or external integrations.
A circular dependency occurs when two modules depend on each other directly or indirectly.

Example:

// a.js
const b = require('./b');
module.exports = { name: 'A' };

// b.js
const a = require('./a');
module.exports = { name: 'B' };
This can cause incomplete exports or undefined values.

Solution:

  1. Refactor shared logic into a new module
  2. Use dependency injection
  3. Lazy-load the dependency inside a function
// a.js - Lazy loading solution
function getB() {
  const b = require('./b');
  return b.name;
}

Why:

To prevent runtime bugs due to incomplete module initialization.

When:

When modules reference each other’s exports.

Where:

Common in large codebases with intertwined controllers/services.
Split routes by feature or module to avoid one large routes file.

Example:

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

router.get('/', userController.getAllUsers);
router.post('/', userController.createUser);

module.exports = router;

// app.js
const express = require('express');
const app = express();
app.use('/users', require('./routes/userRoutes'));

Why:

Keeps routing organized and readable.

When:

As soon as routes exceed 4–5 endpoints.

Where:

Used in all Express/Fastify REST APIs.
FolderPurposeExample
configCentralized configurationsDB, environment setup
utilsReusable functionsformatters, loggers, date handlers
helpersRequest-specific helper logicvalidation, response shaping

Example:

// utils/logger.js
module.exports = (msg) => console.log(`[LOG]: ${msg}`);

Why:

Encourages DRY (Don’t Repeat Yourself) coding.

When:

Used in any mid-size project to centralize repetitive logic.

Where:

Globally accessible via imports across layers.
In microservices:
  • Each service owns a single business domain
  • Communicates via APIs, queues, or message brokers (e.g., RabbitMQ, Kafka)
  • Uses its own database

Example Architecture:

/user-service
/order-service
/notification-service
Each runs independently and communicates via REST or async queues.

Why:

To achieve scalability, fault isolation, and independent deployment.

When:

When the app grows large and needs distributed scaling.

Where:

Used in large enterprise systems (e.g., Uber, Netflix architectures).

12. Node.js Testing and Debugging

Testing ensures your application works as expected, remains stable during code changes, and helps catch bugs early before deployment.

Types of testing in Node.js:

  1. Unit Testing – Tests individual functions or modules
    • Tools: Jest, Mocha, Chai
    • Example: testing a utility function like calculateTax()
  2. Integration Testing – Tests multiple components working together
    • Example: testing API endpoints interacting with database and services
  3. End-to-End (E2E) Testing – Simulates real user scenarios
    • Tools: Supertest, Cypress
  4. Regression Testing – Ensures new code doesn’t break existing functionality
  5. Performance / Load Testing – Evaluates API performance under stress
    • Tools: Artillery, K6

Why:

Improves code reliability and confidence in deployment.

When:

After each module development or before merging into main branch.

Where:

Applied across APIs, database queries, and business logic layers.
Unit testing focuses on testing individual functions or components in isolation.

Why:

To verify that each unit of your code performs as intended without depending on external systems.

When:

During development or before integration.

Example using Jest:

// tax.js
function calculateTax(amount) {
  if (amount <= 0) return 0;
  return amount * 0.1;
}
module.exports = calculateTax;

// tax.test.js
const calculateTax = require('./tax');

test('should calculate 10% tax', () => {
  expect(calculateTax(100)).toBe(10);
});

test('should return 0 for invalid amount', () => {
  expect(calculateTax(-50)).toBe(0);
});

Where:

Stored inside a __tests__ folder or with .test.js extension inside the same module directory.
Use Supertest (with Jest or Mocha) to test your Express APIs.

Why:

It performs real HTTP requests to your routes and verifies the response.

Example:

const request = require('supertest');
const app = require('../app');

describe('GET /users', () => {
  it('should return all users', async () => {
    const res = await request(app).get('/users');
    expect(res.statusCode).toEqual(200);
    expect(Array.isArray(res.body)).toBe(true);
  });
});

When:

Run after every build or deploy via CI/CD pipelines.

Where:

Tests are usually stored in /tests/api or /__integration__/.
Use mocking to isolate your code from external APIs, databases, or modules.

Why:

To ensure tests run fast, deterministically, and don’t depend on network or environment.

When:

When your code calls APIs, databases, or third-party SDKs.

Example using Jest Mocks:

// userService.js
const axios = require('axios');
async function getUser(id) {
  const res = await axios.get(`https://jsonplaceholder.typicode.com/users/${id}`);
  return res.data;
}
module.exports = getUser;

// userService.test.js
jest.mock('axios');
const axios = require('axios');
const getUser = require('./userService');

test('should return mocked user', async () => {
  axios.get.mockResolvedValue({ data: { id: 1, name: 'John' } });
  const user = await getUser(1);
  expect(user.name).toBe('John');
});

Where:

In any module that uses third-party dependencies like AWS SDK, Stripe, or Axios.
Debugging helps track down bugs, performance issues, and unexpected behaviors.

Common methods:

  1. Console logging: Quick and easy but not ideal for large projects.
    console.log('User:', user);
    
  2. Node Inspector / Chrome DevTools: Run app with:
    node --inspect app.js
    
    Then open chrome://inspect → Attach debugger → Add breakpoints.
  3. VS Code Debugger: Add a launch.json config:
    {
      "type": "node",
      "request": "launch",
      "program": "${workspaceFolder}/app.js"
    }
    
  4. PM2 Logs: For production debugging:
    pm2 logs
    

When:

During development or after reproducing a bug reported from QA.

Where:

You can debug application logic, event loops, or async operations.
Use structured error handling so the system remains stable even when exceptions occur.

Why:

Uncaught exceptions can crash the server.

Example (Async/Await):

app.get('/user/:id', async (req, res, next) => {
  try {
    const user = await User.findById(req.params.id);
    if (!user) throw new Error('User not found');
    res.json(user);
  } catch (err) {
    next(err);
  }
});

// global error handler
app.use((err, req, res, next) => {
  console.error(err.message);
  res.status(500).json({ error: err.message });
});

When:

Apply globally via middleware for every route.

Where:

At controller level or in a centralized error handler.
  • Node.js Inspector – built-in debugger for step-through debugging
  • Chrome DevTools – UI-based debugging
  • VS Code Debugger – integrated IDE tool
  • PM2 – process manager for logs and metrics
  • Clinic.js – performance profiler
  • Winston / Pino – structured logging libraries

Why:

They help in isolating performance bottlenecks, memory leaks, and runtime errors.

Where:

Use locally (VS Code/Chrome) or in production (PM2, Winston).
Test coverage measures how much of your code is executed during tests.

Tool: Jest provides built-in coverage reports.

jest --coverage
This outputs:
Statements   : 92%
Branches     : 85%
Functions    : 90%
Lines        : 93%

CI/CD Integration (GitHub Actions example):

name: Node.js CI
on: [push]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - run: npm install
      - run: npm test -- --coverage

Why:

Automatically ensures all commits pass tests before merging.

When:

Triggered on every pull request or push to main branch.
Memory leaks occur when memory is allocated but never freed.

Detection Steps:

  1. Monitor heap usage:
    setInterval(() => console.log(process.memoryUsage()), 5000);
    
  2. Use Chrome DevTools:
    • Run node --inspect
    • Open Heap Snapshots → Compare over time
  3. Use Clinic.js or Memwatch-next

Common causes:

  • Global variables
  • Unclosed timers
  • Unreleased event listeners

Fix:

  • Use WeakMap for temporary references
  • Remove listeners:
    emitter.removeAllListeners();
    
  • Close database connections

13. Database Design

It depends on data structure, relationships, and query patterns.
SQL (Relational)NoSQL (Document/Key-Value)
Structured schema (tables, columns)Flexible schema (JSON, documents)
Strong relationships (JOINs)Denormalized, nested data
ACID transactionsEventual consistency
Example: PostgreSQL, MySQLExample: MongoDB, DynamoDB

Example Use Cases:

  • SQL: Financial systems, HR platforms (strong relationships)
  • NoSQL: E-commerce product catalogs, social feeds (flexible schema)

Why:

SQL enforces strict integrity, NoSQL provides scalability.

When:

Choose SQL when data relations are strong; NoSQL for fast-growing, schema-less data.

Where:

SQL → transactional layer; NoSQL → analytics or caching layer.
  • Normalization: Process of organizing data to reduce redundancy and improve consistency
  • Denormalization: Combining related data into a single structure to improve read performance

Example:

Normalized (two tables):
Users: (id, name)
Orders: (id, user_id, product)
Denormalized (single collection in MongoDB):
{
  "userId": 1,
  "name": "Ali",
  "orders": [
    { "product": "Laptop" },
    { "product": "Mouse" }
  ]
}

Why:

Normalization improves consistency; denormalization improves read speed.

When:

Normalize for frequent writes, denormalize for heavy reads.

Where:

E.g., OLTP → normalized; OLAP/NoSQL → denormalized.
An index is a data structure (like a B-tree or hash) that allows fast lookups on columns or fields.

Example (MongoDB & SQL):

// MongoDB
db.users.createIndex({ email: 1 });

// SQL
CREATE INDEX idx_email ON users(email);

Why:

It avoids full collection/table scans.

When:

Use on frequently filtered or sorted fields (like email, createdAt).

Where:

Use on read-heavy collections/tables.
Indexes slow down writes (inserts/updates) and increase memory usage. Avoid over-indexing — only index what you query often.
A transaction ensures a group of operations succeeds or fails as one unit (ACID — Atomicity, Consistency, Isolation, Durability).

Example (PostgreSQL with Sequelize):

const t = await sequelize.transaction();

try {
  await User.create({ name: 'Ali' }, { transaction: t });
  await Order.create({ userId: 1, product: 'Phone' }, { transaction: t });
  await t.commit();
} catch (err) {
  await t.rollback();
}

Why:

Prevents partial updates when one step fails.

When:

Use for multi-table or dependent operations (e.g., payments, inventory).

Where:

Implement in service layer functions handling multi-step DB operations.
Step-by-step approach:
  1. Use EXPLAIN or explain() to inspect query execution plan
  2. Add proper indexes
  3. Avoid SELECT *; specify columns
  4. Paginate large queries
  5. Cache repetitive queries

Example (MongoDB):

db.orders.find({ status: 'completed' }).explain('executionStats');

Example (Redis caching):

const cached = await redis.get('orders');
if (cached) return JSON.parse(cached);

const data = await Order.find({ status: 'completed' });
await redis.setEx('orders', 3600, JSON.stringify(data));

Why:

Optimized queries save cost and improve API response times.

When:

Apply during scaling or under heavy load.

Where:

Inside data-access layer (repositories).
Common types:
  • One-to-One: User ↔ Profile
  • One-to-Many: User → Orders
  • Many-to-Many: Students ↔ Courses

Example (MongoDB - embedding vs referencing):

// Referencing (normalized)
const order = { userId: ObjectId("..."), product: "Laptop" };

// Embedding (denormalized)
const user = { name: "Ali", orders: [{ product: "Laptop" }] };

Why:

Embedding improves read speed; referencing saves space.

When:

Embed for frequent reads; reference for frequent writes.

Where:

Schema design phase, based on access patterns.
Offset-based (SQL):
SELECT * FROM orders LIMIT 10 OFFSET 20;
Cursor-based (MongoDB or large datasets):
db.orders.find({ _id: { $gt: lastId } }).limit(10);

Why:

Offset is simple but inefficient for large data; cursor-based is faster.

When:

Cursor-based for infinite scrolling or APIs.

Where:

Implement in API layer — GET /orders?cursor=<id>
Migrations are version control for your database schema.

Example (with Sequelize):

npx sequelize migration:generate --name add_isActive_to_users

Example (migration file):

export async function up(queryInterface, Sequelize) {
  await queryInterface.addColumn('Users', 'isActive', Sequelize.BOOLEAN);
}

Why:

Ensures consistent schema across environments (dev, staging, prod).

When:

Whenever adding/removing/modifying columns or constraints.

Where:

Stored in /migrations folder, managed by ORM/CLI.
Techniques:
  1. Read replicas — offload read traffic
  2. Sharding — partition data by key (e.g., userId)
  3. Caching — Redis/Memcached for frequent reads
  4. Connection pooling — reuse DB connections

Example:

const pool = new Pool({
  max: 10, // limit active connections
  idleTimeoutMillis: 30000,
});

Why:

Prevents bottlenecks under high load.

When:

Beyond 10k+ users or concurrent reads.

Where:

Database + ORM configuration level.
Consistency ensures all services see the same data.

Patterns:

  • Two-phase commit (2PC) for strict consistency
  • Eventual consistency for scalability
  • Sagas pattern for distributed transactions

Example (Sagas):

// Place order -> reduce stock -> charge payment
// If payment fails -> rollback stock -> cancel order

Why:

Keeps data correct even across microservices.

When:

In event-driven or microservice architectures.

Where:

Implement at service orchestration level.
ACID (SQL)BASE (NoSQL)
AtomicityBasically Available
ConsistencySoft-state
IsolationEventual consistency
Durability

Why:

ACID ensures reliable transactions; BASE ensures availability at scale.

When:

Use ACID for critical systems (banking), BASE for high-volume systems (social media).

Where:

Choose based on business priority: consistency vs availability.

14. API Design & Best Practices

REST (Representational State Transfer) is an architectural style for building scalable web services that communicate over HTTP.

Core Principles:

  1. Statelessness → Server doesn’t store client state between requests
  2. Uniform Interface → Consistent structure for all endpoints (/users, /products)
  3. Client-Server Separation → Independent evolution of frontend & backend
  4. Cacheable → Responses can be cached for performance
  5. Layered System → Requests pass through intermediaries like load balancers or proxies

Example:

// RESTful Routes
GET    /users          // Fetch users
POST   /users          // Create user
GET    /users/:id      // Get user by ID
PUT    /users/:id      // Update user
DELETE /users/:id      // Delete user

Why:

REST’s simplicity makes it ideal for large-scale distributed systems.

When:

Use for stateless communication (e.g., SaaS apps).

Where:

Commonly implemented with Express.js or NestJS.
Follow best practices:

Use modular folder structure:

src/
  controllers/
  routes/
  services/
  models/
  middlewares/
  1. Follow Controller-Service-Repository pattern
  2. Use async/await and central error handling
  3. Add pagination, filtering, sorting

Example:

// Controller
export const getUsers = async (req, res, next) => {
  const { page = 1, limit = 10 } = req.query;
  const users = await userService.getAll({ page, limit });
  res.json(users);
};

Why:

Separation of concerns ensures scalability and testability.

When:

For medium to large projects.

Where:

Apply across all route modules for consistency.
MethodPurposeExample
PUTReplace the entire resource{ "name": "Ali", "email": "x@x.com" }
PATCHUpdate part of a resource{ "email": "new@x.com" }

Example:

app.patch('/users/:id', async (req, res) => {
  const user = await User.findByIdAndUpdate(req.params.id, req.body, { new: true });
  res.json(user);
});

Why:

Use PATCH for partial updates to avoid overwriting data.

When:

Frontend sends only changed fields.

Where:

In APIs supporting user profile updates, etc.
Centralized error handling ensures cleaner code and consistent responses.

Example:

// Middleware
function errorHandler(err, req, res, next) {
  res.status(err.status || 500).json({ message: err.message });
}

// Controller
if (!email) throw { status: 400, message: 'Email is required' };

// Validation (Zod / Joi / Express Validator):
import { z } from 'zod';
const userSchema = z.object({ email: z.string().email(), password: z.string().min(6) });
userSchema.parse(req.body);

Why:

Prevents invalid or unsafe input.

When:

Before DB operations or external API calls.

Where:

Middleware or controller level.
API versioning ensures backward compatibility when updating endpoints.

Methods:

  1. URL versioning (most common):
    /api/v1/users
    /api/v2/users
    
  2. Header versioning:
    Accept: application/vnd.myapp.v2+json
    

Example:

app.use('/api/v1', v1Routes);
app.use('/api/v2', v2Routes);

Why:

Prevents breaking changes for existing clients.

When:

On major updates or endpoint restructuring.

Where:

Route definition layer.
  1. Authentication & Authorization (JWT, OAuth2)
  2. Input validation (prevent SQL/NoSQL injection)
  3. Rate limiting (prevent brute-force)
  4. CORS control
  5. HTTPS for encryption

Example (rate limiting):

import rateLimit from 'express-rate-limit';
app.use(rateLimit({ windowMs: 15 * 60 * 1000, max: 100 }));

Why:

Prevents abuse, leaks, and attacks.

When:

Always — security should be baked in early.

Where:

Applied globally or per route.
An idempotent method gives the same result even if called multiple times.
MethodIdempotent?Example
GET✅ YesFetching user
PUT✅ YesUpdating same data
DELETE✅ YesDeleting same resource again
POST❌ NoCreates a new record each time

Example:

DELETE /users/5
// Returns 204 No Content even if user was already deleted

Why:

Idempotency ensures reliability in retry scenarios.

When:

Especially in payment APIs or distributed systems.

Where:

Route and controller logic level.

Example:

GET /products?page=2&limit=10&sort=price:desc&category=shoes

Implementation:

const { page = 1, limit = 10, sort, category } = req.query;
const filter = category ? { category } : {};
const products = await Product.find(filter)
  .skip((page - 1) * limit)
  .limit(limit)
  .sort(sort.replace(':', ' '));

Why:

Enhances performance and user experience.

When:

For list-based data (users, products, posts).

Where:

In every list API endpoint.
Use OpenAPI/Swagger for auto-generated documentation.

Example:

npm install swagger-ui-express swagger-jsdoc

import swaggerUi from 'swagger-ui-express';
import swaggerJsDoc from 'swagger-jsdoc';

const specs = swaggerJsDoc({
  definition: { openapi: '3.0.0', info: { title: 'API Docs', version: '1.0.0' } },
  apis: ['./routes/*.js'],
});
app.use('/docs', swaggerUi.serve, swaggerUi.setup(specs));

Why:

Improves onboarding and collaboration with frontend teams.

When:

As soon as APIs are stable.

Where:

Separate /docs route.
Rate limiting: restricts number of requests per user/IP
Throttling: delays excessive requests

Example (Redis-based):

import rateLimit from 'express-rate-limit';
app.use('/api', rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 100,
  message: 'Too many requests, try again later.',
}));

Why:

Prevents DDoS and API abuse.

When:

For login or public APIs.

Where:

At gateway or middleware layer.
CodeMeaningUse Case
200OKSuccessful GET
201CreatedResource created (POST)
204No ContentSuccessful DELETE
400Bad RequestValidation error
401UnauthorizedMissing/invalid token
403ForbiddenAccess denied
404Not FoundResource doesn’t exist
500Internal Server ErrorUnexpected error

Example:

res.status(201).json({ message: 'User created successfully' });

Why:

Clear status codes improve debugging and API usability.

When:

Always respond with meaningful HTTP codes.

Where:

Controller layer.
RESTGraphQL
Multiple endpointsSingle endpoint /graphql
Fixed response shapeClient defines response
Over-fetching commonFetch only needed fields
Simpler cachingComplex but flexible queries

Example:

// GraphQL query
query {
  user(id: "1") {
    name
    posts {
      title
    }
  }
}

Why:

GraphQL avoids under/over-fetching.

When:

For complex UIs needing custom data shapes (like dashboards).

Where:

Use Apollo Server or Yoga with Node.js.

15. Caching and Performance Optimization

Caching is the process of storing frequently accessed data in a fast-access storage layer (like memory) so that future requests for that data can be served faster.

Why:

  • Reduces response time
  • Minimizes database load
  • Improves scalability and user experience

When to use:

When you have repetitive, read-heavy operations such as:
  • Fetching static product details
  • Returning popular posts
  • Computing costly aggregations

Where:

Typically used:
  • Between the API and the database
  • At CDN level for static assets
  • In-memory (e.g., Redis, Node cache) for API data

Example:

const express = require('express');
const redis = require('redis');
const fetch = require('node-fetch');

const client = redis.createClient();
const app = express();

app.get('/posts', async (req, res) => {
  const cachedPosts = await client.get('posts');
  if (cachedPosts) return res.json(JSON.parse(cachedPosts));

  const response = await fetch('https://jsonplaceholder.typicode.com/posts');
  const data = await response.json();

  client.setEx('posts', 3600, JSON.stringify(data)); // cache for 1 hour
  res.json(data);
});
The first request fetches data from the API, but subsequent ones serve from Redis memory, reducing external API calls.

1. In-memory cache:

  • Stored in Node process memory using packages like node-cache or lru-cache
  • Fastest but not shared across multiple server instances
  • Best for: Single-instance apps, computed results
const NodeCache = require("node-cache");
const myCache = new NodeCache({ stdTTL: 600 });
myCache.set("key", "value");
console.log(myCache.get("key")); // 'value'

2. Distributed cache (Redis / Memcached):

  • External in-memory databases shared across multiple app instances
  • Best for: Scalable and load-balanced applications

3. Browser caching / CDN caching:

  • For static files like images, scripts, and CSS
  • Best for: Reducing load time for front-end users

4. Application-level caching:

  • Using HTTP headers like Cache-Control, ETag, or response-level caching middleware
By profiling and monitoring the app’s runtime performance using tools such as:

Node’s built-in profiler:

node --inspect app.js
Opens Chrome DevTools for debugging and profiling.

Performance monitoring tools:

  • PM2 monitoring dashboard
  • New Relic, Datadog, or AppDynamics for production-level insights

Steps to identify bottlenecks:

  1. Measure response time and CPU usage
  2. Analyze event loop lag (using clinic.js or node —inspect)
  3. Check for slow database queries and missing indexes
  4. Profile memory leaks via heap snapshots

When:

Always perform during load testing before production deployment.

1. Use asynchronous code properly:

Avoid blocking the event loop by using non-blocking async operations.
// BAD: Blocking
const fs = require('fs');
const data = fs.readFileSync('file.txt');

// GOOD: Non-blocking
fs.readFile('file.txt', (err, data) => { ... });

2. Enable GZIP compression:

Compress HTTP responses using middleware like compression.
const compression = require('compression');
app.use(compression());

3. Use Redis or in-memory cache:

Cache frequent DB queries to reduce load.

4. Cluster your Node process:

Utilize multiple CPU cores using cluster or PM2.
const cluster = require('cluster');
const os = require('os');

if (cluster.isPrimary) {
  os.cpus().forEach(() => cluster.fork());
} else {
  require('./server');
}

5. Optimize database queries:

Use proper indexes, projections, and pagination.

6. Use streaming for large data:

Instead of loading entire data into memory.
const fs = require('fs');
fs.createReadStream('largeFile.txt').pipe(res);

7. Use load balancing & reverse proxies (Nginx):

Helps distribute load across multiple Node instances.
Redis is an in-memory data store that can be used for:
  • Caching responses
  • Session storage
  • Pub/Sub systems
  • Rate limiting

Why Redis:

  • Very fast (stores data in RAM)
  • Persistent (can save snapshots to disk)
  • Supports TTL (time-to-live) expiration

Example:

const redis = require('redis');
const client = redis.createClient();

client.connect();

async function getCachedUser(id) {
  const cachedUser = await client.get(`user:${id}`);
  if (cachedUser) return JSON.parse(cachedUser);

  const user = await getUserFromDatabase(id);
  await client.setEx(`user:${id}`, 600, JSON.stringify(user)); // cache 10min
  return user;
}

When:

When your system frequently requests the same data from a slow data source like MongoDB or an external API.
Node.js is single-threaded, so any CPU-intensive task can block the event loop and delay other requests.

Example of blocking:

// BAD
app.get('/heavy', (req, res) => {
  let sum = 0;
  for (let i = 0; i < 1e9; i++) sum += i; // blocks everything
  res.send('Done');
});

Fix:

Use worker threads or child processes for CPU-heavy operations.
const { Worker } = require('worker_threads');
app.get('/heavy', (req, res) => {
  const worker = new Worker('./heavyTask.js');
  worker.on('message', msg => res.send(msg));
});
Rate limiting ensures a user doesn’t overwhelm the server with too many requests in a short period.

When to use:

For APIs prone to abuse — login, search, or payment endpoints.

Example using express-rate-limit:

const rateLimit = require('express-rate-limit');

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100,
});

app.use(limiter);
You can also store rate limit counters in Redis for distributed environments.
You can monitor heap usage using:
console.log(process.memoryUsage());
or by using Chrome DevTools:
  • Run app with node --inspect
  • Open chrome://inspect
  • Take Heap Snapshots and compare over time

When leaks occur:

  • Global variables not freed
  • Large cached data without TTL
  • Event listeners not removed

Where to fix:

Ensure proper cleanup using:
emitter.removeAllListeners();
cache.del('largeKey');

Conclusion & Interview Tips

This comprehensive guide covers essential MERN Stack interview questions across all four technologies. The MERN stack combines MongoDB, Express.js, React, and Node.js to create powerful full-stack web applications.

Key Interview Preparation Tips

  • Master fundamentals before advanced topics
  • Build full-stack projects for hands-on experience
  • Understand how technologies integrate
  • Focus on best practices and security
  • Practice explaining your thought process
  • Review recent updates and ecosystem changes
  • Prepare for coding challenges and system design
  • Study common architectural patterns

During the Interview

  • Ask clarifying questions before coding
  • Think aloud to show your problem-solving approach
  • Consider edge cases and error handling
  • Discuss trade-offs in your solutions
  • Be honest about what you don’t know
  • Show enthusiasm for learning

Technical Skills to Demonstrate

  • Database design and query optimization
  • RESTful API development
  • Component architecture and state management
  • Asynchronous programming patterns
  • Security best practices
  • Performance optimization techniques
  • Testing and debugging strategies
  • Deployment and DevOps basics
Remember that interviews assess not just technical knowledge but also problem-solving ability, communication skills, and cultural fit. Be confident, stay calm, and demonstrate your passion for web development.
Good luck with your MERN Stack interviews!