Full-Stack Web Programming
Comprehensive Exam-Ready Notes
React | Redux | Node/Express | MongoDB | Auth | Architecture
Prepared for mid-level examination
1. React / Frontend
1.1 Components
A component is a reusable, self-contained piece of UI. React has two types:
• Functional components (modern, preferred): JavaScript functions that return JSX
• Class components (legacy): ES6 classes extending [Link] with a render()
method
// Functional component
function Greeting({ name }) {
return <h1>Hello, {name}</h1>;
}
// Class component (legacy, but may appear in exams)
class Greeting extends [Link] {
render() {
return <h1>Hello, {[Link]}</h1>;
}
}
EXAM TIP: Functional components cannot use lifecycle methods directly. They use hooks
instead. If asked about componentDidMount, the equivalent hook is useEffect with an empty
dependency array.
1.2 Props
Props (properties) are read-only inputs passed from parent to child. They flow one way: parent to
child only.
• Props are immutable inside the receiving component
• Default values can be set with destructuring defaults or defaultProps
• Props can be any JS value: strings, numbers, arrays, objects, functions, even other
components
function UserCard({ name, age, onDelete }) {
return (
<div>
<h2>{name}, {age}</h2>
<button onClick={onDelete}>Delete</button>
</div>
);
}
// Usage
<UserCard name="Eshaal" age={20} onDelete={() => handleDelete(id)} />
COMMON PITFALL: Mutating props inside a child component will NOT cause errors
immediately but breaks React's one-way data flow and leads to unpredictable bugs. Never do
[Link] = 'new'.
1.3 State
State is mutable data managed inside a component. When state changes, the component re-
renders.
const [count, setCount] = useState(0);
// CORRECT: Use the setter function
setCount(count + 1);
// CORRECT: Use callback form when new state depends on previous
setCount(prev => prev + 1);
// WRONG: Never mutate state directly
count = 5; // This will NOT trigger a re-render
EXAM TIP: useState is asynchronous. If you call setCount(count + 1) twice in a row, both use
the same stale 'count'. Use the callback form setCount(prev => prev + 1) to chain updates
correctly.
State vs Props comparison
Feature Props State
Owner Parent component Component itself
Mutable? No (read-only) Yes (via setter)
Triggers re-render? Yes (when parent re-renders) Yes (when setter called)
Direction Parent to child Internal only
1.4 JSX
JSX is a syntax extension that looks like HTML but compiles to [Link]() calls. Key
rules:
• Must return a single root element (use <div> or <> fragment)
• Use className instead of class
• Use htmlFor instead of for
• All tags must be closed, including self-closing: <img />, <br />, <input />
• JavaScript expressions go inside curly braces { }
• Inline styles use double curly braces with camelCase: style={{ backgroundColor: 'red' }}
COMMON PITFALL: Forgetting to close self-closing tags (<img> instead of <img />) causes JSX
compilation errors. HTML is forgiving, JSX is not.
1.5 Hooks: useState and useEffect
useState
Declares a state variable. Returns [currentValue, setterFunction].
const [items, setItems] = useState([]);
const [formData, setFormData] = useState({ name: '', email: '' });
// Updating objects: always spread the previous state
setFormData(prev => ({ ...prev, name: 'Eshaal' }));
// Updating arrays: never mutate, create new array
setItems(prev => [...prev, newItem]); // add
setItems(prev => [Link](i => [Link] !== id)); // remove
setItems(prev => [Link](i => // update
[Link] === id ? { ...i, done: true } : i
));
useEffect
Runs side effects after render. Replaces componentDidMount, componentDidUpdate, and
componentWillUnmount.
// Runs on EVERY render (rarely what you want)
useEffect(() => { [Link]('rendered'); });
// Runs ONCE on mount (empty dependency array)
useEffect(() => {
fetchData();
}, []);
// Runs when 'query' changes
useEffect(() => {
search(query);
}, [query]);
// Cleanup function (runs before next effect or on unmount)
useEffect(() => {
const timer = setInterval(() => tick(), 1000);
return () => clearInterval(timer); // cleanup
}, []);
EXAM TIP: The dependency array controls when the effect runs. Missing dependencies = stale
closures. An empty [] means 'run once'. No array at all means 'run every render'. This is the most
tested hook concept in exams.
1.6 Conditional rendering
// Ternary (inline)
{isLoggedIn ? <Dashboard /> : <Login />}
// Logical AND (show or nothing)
{error && <p className="error">{error}</p>}
// Early return pattern
function Profile({ user }) {
if (!user) return <p>Please log in</p>;
return <h1>{[Link]}</h1>;
}
1.7 Lists and keys
Use .map() to render arrays. Every list item needs a unique, stable key prop.
{[Link](user => (
<li key={[Link]}>{[Link]}</li>
))}
COMMON PITFALL: Never use array index as key if the list can be reordered, filtered, or items
added/removed. It causes rendering bugs and lost state. Use a unique ID from your data.
1.8 Forms and validation
Controlled components
The React state is the single source of truth. Every input change updates state via onChange.
function SignupForm() {
const [form, setForm] = useState({ email: '', password: '' });
const [errors, setErrors] = useState({});
const validate = () => {
const errs = {};
if () [Link] = 'Invalid email';
if ([Link] < 6) [Link] = 'Min 6 characters';
setErrors(errs);
return [Link](errs).length === 0;
};
const handleSubmit = (e) => {
[Link](); // Prevent page reload!
if (validate()) {
// submit to API
}
};
return (
<form onSubmit={handleSubmit}>
<input
value={[Link]}
onChange={e => setForm(p => ({ ...p, email: [Link] }))}
/>
{[Link] && <span>{[Link]}</span>}
<input
type="password"
value={[Link]}
onChange={e => setForm(p => ({ ...p, password: [Link] }))}
/>
{[Link] && <span>{[Link]}</span>}
<button type="submit">Sign Up</button>
</form>
);
}
EXAM TIP: Always call [Link]() in form submit handlers. Without it, the browser
reloads the page and you lose all React state. This is the number one forgotten line in exam
code.
1.9 API calls (fetch and axios)
// Using fetch inside useEffect
useEffect(() => {
const loadUsers = async () => {
try {
const res = await fetch('[Link]
const data = await [Link]();
setUsers(data);
} catch (err) {
setError('Failed to load users');
}
};
loadUsers();
}, []);
// POST request with fetch
const createUser = async (userData) => {
const res = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: [Link](userData),
});
return [Link]();
};
// Same POST with axios (shorter)
const createUser = async (userData) => {
const { data } = await [Link]('/api/users', userData);
return data;
};
COMMON PITFALL: You cannot make useEffect's callback async directly. Wrap the async logic
in an inner function and call it. useEffect(async () => ...) is WRONG.
1.10 Event handling
React uses synthetic events (camelCase). Common events: onClick, onChange, onSubmit,
onKeyDown, onFocus, onBlur.
// Passing arguments to handlers
<button onClick={() => handleDelete([Link])}>Delete</button>
// Without arrow function (no argument)
<button onClick={handleClick}>Click</button>
// WRONG: This calls immediately on render
<button onClick={handleDelete([Link])}>Delete</button>
1.11 Grid/list view toggling
const [viewMode, setViewMode] = useState('list');
return (
<div>
<button onClick={() => setViewMode('list')}>List</button>
<button onClick={() => setViewMode('grid')}>Grid</button>
<div className={viewMode === 'grid' ? 'grid-container' : 'list-
container'}>
{[Link](item => (
<ItemCard key={[Link]} item={item} viewMode={viewMode} />
))}
</div>
</div>
);
2. Redux (Redux Toolkit)
2.1 Core concepts
Redux provides a single, centralized store for global state. Data flows in one direction: UI dispatches
actions, reducers process them, store updates, UI re-renders.
The Redux data flow
• Store: Single object holding all application state
• Action: Plain object with a type field describing what happened: { type: 'todos/add', payload:
'Buy milk' }
• Reducer: Pure function that takes (state, action) and returns new state. Never mutates.
• Dispatch: Method to send an action to the store: dispatch(addTodo('Buy milk'))
• Selector: Function to extract specific data from store: useSelector(state =>
[Link])
2.2 Creating a slice (Redux Toolkit)
A slice bundles the reducer, actions, and initial state for one feature.
// features/[Link]
import { createSlice } from '@reduxjs/toolkit';
const todosSlice = createSlice({
name: 'todos',
initialState: {
items: [],
filter: 'all',
loading: false,
},
reducers: {
addTodo: (state, action) => {
// Redux Toolkit uses Immer internally!
// This LOOKS like mutation but creates a new object behind the
scenes
[Link]({ id: [Link](), text: [Link], done:
false });
},
toggleTodo: (state, action) => {
const todo = [Link](t => [Link] === [Link]);
if (todo) [Link] = ![Link];
},
removeTodo: (state, action) => {
[Link] = [Link](t => [Link] !== [Link]);
},
setFilter: (state, action) => {
[Link] = [Link];
},
},
});
export const { addTodo, toggleTodo, removeTodo, setFilter } =
[Link];
export default [Link];
EXAM TIP: Redux Toolkit uses Immer under the hood, so you CAN write 'mutating' code like
[Link]() inside createSlice reducers. But in plain Redux (without Toolkit), you must
ALWAYS return a new object. Know which version your exam covers.
2.3 Configuring the store
// [Link]
import { configureStore } from '@reduxjs/toolkit';
import todosReducer from './features/todosSlice';
import authReducer from './features/authSlice';
export const store = configureStore({
reducer: {
todos: todosReducer,
auth: authReducer,
},
});
// [Link] - Wrap app with Provider
import { Provider } from 'react-redux';
import { store } from './store';
<Provider store={store}>
<App />
</Provider>
2.4 Using Redux in components
import { useSelector, useDispatch } from 'react-redux';
import { addTodo, toggleTodo, removeTodo } from './features/todosSlice';
function TodoList() {
const todos = useSelector(state => [Link]);
const filter = useSelector(state => [Link]);
const dispatch = useDispatch();
const filteredTodos = [Link](t => {
if (filter === 'active') return ![Link];
if (filter === 'completed') return [Link];
return true;
});
return (
<div>
<button onClick={() => dispatch(addTodo('New task'))}>Add</button>
{[Link](todo => (
<div key={[Link]}>
<span
onClick={() => dispatch(toggleTodo([Link]))}
style={{ textDecoration: [Link] ? 'line-through' : 'none' }}
>
{[Link]}
</span>
<button onClick={() => dispatch(removeTodo([Link]))}>X</button>
</div>
))}
</div>
);
}
2.5 Immutability (without Toolkit)
In plain Redux, every reducer must return a brand new state object. Here is how to handle common
operations:
// ADD to array
return { ...state, items: [...[Link], newItem] };
// REMOVE from array
return { ...state, items: [Link](i => [Link] !==
[Link]) };
// UPDATE in array
return {
...state,
items: [Link](i =>
[Link] === [Link] ? { ...i, ...[Link] } : i
),
};
// UPDATE nested object
return {
...state,
user: { ...[Link], name: [Link] },
};
COMMON PITFALL: The spread operator only does a shallow copy. If you have deeply nested
state, you need to spread at every level. [Link] = 'Lahore' is mutation even if
you spread the top level.
3. [Link] / Express
3.1 Basic server setup
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
const cookieParser = require('cookie-parser');
const app = express();
// Middleware
[Link](cors({ origin: '[Link] credentials: true }));
[Link]([Link]()); // Parse JSON request bodies
[Link](cookieParser()); // Parse cookies
[Link]([Link]({ extended: true })); // Parse form data
// Routes
[Link]('/api/users', require('./routes/users'));
[Link]('/api/products', require('./routes/products'));
// Connect to MongoDB, then start server
[Link]('mongodb://localhost:27017/myapp')
.then(() => [Link](5000, () => [Link]('Server on port 5000')))
.catch(err => [Link](err));
3.2 Route design (RESTful CRUD)
Method Route Purpose Request body?
GET /api/users Get all users No
GET /api/users/:id Get one user No
POST /api/users Create user Yes
PUT /api/users/:id Replace user Yes
PATCH /api/users/:id Update fields Yes
DELETE /api/users/:id Delete user No
Full CRUD route file
// routes/[Link]
const router = require('express').Router();
const User = require('../models/User');
// GET all
[Link]('/', async (req, res) => {
try {
const users = await [Link]();
[Link](users);
} catch (err) {
[Link](500).json({ error: [Link] });
}
});
// GET one by ID
[Link]('/:id', async (req, res) => {
try {
const user = await [Link]([Link]);
if (!user) return [Link](404).json({ error: 'Not found' });
[Link](user);
} catch (err) {
[Link](500).json({ error: [Link] });
}
});
// POST create
[Link]('/', async (req, res) => {
try {
const user = await [Link]([Link]);
[Link](201).json(user);
} catch (err) {
[Link](400).json({ error: [Link] });
}
});
// PUT update (full replace)
[Link]('/:id', async (req, res) => {
try {
const user = await [Link]([Link], [Link], {
new: true, // Return updated document
runValidators: true, // Run schema validators
});
if (!user) return [Link](404).json({ error: 'Not found' });
[Link](user);
} catch (err) {
[Link](400).json({ error: [Link] });
}
});
// DELETE
[Link]('/:id', async (req, res) => {
try {
const user = await [Link]([Link]);
if (!user) return [Link](404).json({ error: 'Not found' });
[Link]({ message: 'Deleted' });
} catch (err) {
[Link](500).json({ error: [Link] });
}
});
[Link] = router;
EXAM TIP: findByIdAndUpdate does NOT run validators by default. Always pass
{ runValidators: true }. Also pass { new: true } to get the updated document back, not the old one.
3.3 Middleware
Middleware functions have access to req, res, and next. They execute in order. If a middleware does
not call next(), the request hangs.
// Custom logging middleware
const logger = (req, res, next) => {
[Link](`${[Link]} ${[Link]}`);
next(); // MUST call next() to continue
};
[Link](logger);
// Error-handling middleware (4 parameters!)
[Link]((err, req, res, next) => {
[Link]([Link]);
[Link](500).json({ error: 'Something went wrong' });
});
COMMON PITFALL: Error-handling middleware MUST have exactly 4 parameters (err, req, res,
next) even if you don't use next. Express checks the function signature to identify it as an error
handler.
3.4 Cookies and sessions
// Setting a cookie
[Link]('token', jwtToken, {
httpOnly: true, // Not accessible via JavaScript (XSS protection)
secure: true, // Only sent over HTTPS
sameSite: 'strict', // CSRF protection
maxAge: 3600000, // 1 hour in milliseconds
});
// Reading a cookie (needs cookie-parser middleware)
const token = [Link];
// Clearing a cookie
[Link]('token');
// Session setup (express-session)
const session = require('express-session');
[Link](session({
secret: 'your-secret-key',
resave: false,
saveUninitialized: false,
cookie: { maxAge: 3600000 },
}));
// Using sessions
[Link] = user._id; // Set
const userId = [Link]; // Read
[Link](); // Logout
4. MongoDB
4.1 Connection and schema
// models/[Link]
const mongoose = require('mongoose');
const userSchema = new [Link]({
name: { type: String, required: true, trim: true },
email: { type: String, required: true, unique: true, lowercase:
true },
password: { type: String, required: true, minlength: 6 },
role: { type: String, enum: ['user', 'admin'], default: 'user' },
createdAt: { type: Date, default: [Link] },
});
[Link] = [Link]('User', userSchema);
Key schema options: required, unique, default, enum, minlength, maxlength, min, max, trim,
lowercase, match (regex).
4.2 Relationships: embedded vs referenced
Embedded documents (denormalized)
Store related data inside the parent document. Best when data is read together and the child does
not exist independently.
// Embedded: comments inside a post
const postSchema = new [Link]({
title: String,
body: String,
comments: [{
text: String,
author: String,
date: { type: Date, default: [Link] },
}],
});
// Adding a comment
const post = await [Link](postId);
[Link]({ text: 'Great post!', author: 'Ali' });
await [Link]();
Referenced documents (normalized)
Store a reference (ObjectId) to another collection. Best when data is shared across documents or
grows independently.
// Referenced: orders reference a user
const orderSchema = new [Link]({
user: { type: [Link], ref: 'User', required:
true },
items: [{ product: String, qty: Number, price: Number }],
total: Number,
status: { type: String, default: 'pending' },
});
// Populate to get full user object instead of just the ID
const orders = await [Link]({ user: userId }).populate('user', 'name
email');
// Now orders[0].[Link] works instead of just showing an ObjectId
EXAM TIP: populate() replaces the ObjectId with the actual document. The second argument
selects which fields to include. Without populate, you only get the raw ObjectId string.
4.3 CRUD operations cheatsheet
Operation Mongoose method Returns
Create one [Link](data) Created document
Create many [Link]([...]) Array of documents
Read all [Link](filter) Array
Read one [Link](id) Document or null
Read one (query) [Link]({ email }) Document or null
Update one [Link](id, update, Document
opts)
Delete one [Link](id) Deleted document
Count [Link](filter) Number
Useful query methods
// Chaining
const results = await [Link]({ price: { $gte: 10, $lte: 100 } })
.sort({ price: 1 }) // 1 = ascending, -1 = descending
.limit(20)
.skip(0) // For pagination: skip = (page - 1) * limit
.select('name price'); // Only return these fields
// Search with regex
const results = await [Link]({
name: { $regex: searchTerm, $options: 'i' }, // case-insensitive
});
5. Full-Stack Integration
5.1 Front-to-backend communication
The frontend (React) communicates with the backend (Express) via HTTP requests. The backend
processes the request, talks to MongoDB, and returns JSON.
Flow: React Component -> fetch/axios -> Express Route -> Mongoose Model -> MongoDB ->
Response -> Update State/Redux
5.2 Complete CRUD example with Redux
// features/[Link]
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';
const API = '[Link]
// Async thunks
export const fetchProducts = createAsyncThunk('products/fetchAll', async ()
=> {
const { data } = await [Link](API);
return data;
});
export const createProduct = createAsyncThunk('products/create', async
(product) => {
const { data } = await [Link](API, product);
return data;
});
export const deleteProduct = createAsyncThunk('products/delete', async (id)
=> {
await [Link](`${API}/${id}`);
return id;
});
const productsSlice = createSlice({
name: 'products',
initialState: { items: [], loading: false, error: null },
reducers: {},
extraReducers: (builder) => {
builder
.addCase([Link], (state) => { [Link] = true; })
.addCase([Link], (state, action) => {
[Link] = false;
[Link] = [Link];
})
.addCase([Link], (state, action) => {
[Link] = false;
[Link] = [Link];
})
.addCase([Link], (state, action) => {
[Link]([Link]);
})
.addCase([Link], (state, action) => {
[Link] = [Link](p => p._id !== [Link]);
});
},
});
export default [Link];
5.3 Using thunks in a component
function ProductList() {
const dispatch = useDispatch();
const { items, loading, error } = useSelector(state => [Link]);
useEffect(() => {
dispatch(fetchProducts());
}, [dispatch]);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return (
<div>
{[Link](p => (
<div key={p._id}>
<span>{[Link]} - Rs.{[Link]}</span>
<button onClick={() =>
dispatch(deleteProduct(p._id))}>Delete</button>
</div>
))}
</div>
);
}
EXAM TIP: createAsyncThunk automatically generates pending, fulfilled, and rejected action
types. You handle them in extraReducers, not in reducers. This is the pattern exams love to test.
5.4 Sending credentials with requests
When using cookies for auth, both sides need configuration:
// Frontend: include credentials
fetch('/api/profile', { credentials: 'include' });
// or with axios
[Link] = true;
// Backend: allow credentials in CORS
[Link](cors({ origin: '[Link] credentials: true }));
COMMON PITFALL: If credentials: true is set in CORS, the origin cannot be '*'. You must
specify the exact origin. This is the most common full-stack auth bug.
6. Authentication and Security
6.1 Password hashing with bcrypt
const bcrypt = require('bcrypt');
const SALT_ROUNDS = 10;
// Registration: hash password before saving
[Link]('/register', async (req, res) => {
const { name, email, password } = [Link];
const hashedPassword = await [Link](password, SALT_ROUNDS);
const user = await [Link]({ name, email, password:
hashedPassword });
[Link](201).json({ message: 'User created' });
});
// Login: compare plain password with hash
[Link]('/login', async (req, res) => {
const { email, password } = [Link];
const user = await [Link]({ email });
if (!user) return [Link](401).json({ error: 'Invalid credentials' });
const isMatch = await [Link](password, [Link]);
if (!isMatch) return [Link](401).json({ error: 'Invalid
credentials' });
// Create session or JWT here
[Link] = user._id;
[Link]({ message: 'Login successful', user: { name: [Link] } });
});
EXAM TIP: [Link]() handles salt extraction internally. You never need to store or
manage the salt separately. The salt is embedded in the hash string itself.
6.2 JWT (JSON Web Tokens)
JWTs are stateless tokens. The server signs them on login, the client sends them with every
request, and the server verifies them without needing a database lookup.
const jwt = require('jsonwebtoken');
const SECRET = 'your-secret-key'; // In real apps, use env variable
// Generate token on login
const token = [Link](
{ userId: user._id, role: [Link] }, // Payload
SECRET,
{ expiresIn: '1h' }
);
// Send as httpOnly cookie (most secure)
[Link]('token', token, { httpOnly: true, maxAge: 3600000 });
// OR send in response body (client stores in localStorage)
[Link]({ token });
6.3 Auth middleware (protecting routes)
// middleware/[Link]
const jwt = require('jsonwebtoken');
const auth = (req, res, next) => {
// Get token from cookie or Authorization header
const token = [Link] ||
([Link]?.startsWith('Bearer ')
? [Link](' ')[1]
: null);
if (!token) return [Link](401).json({ error: 'No token provided' });
try {
const decoded = [Link](token, [Link].JWT_SECRET);
[Link] = decoded; // Attach user info to request
next();
} catch (err) {
[Link](401).json({ error: 'Invalid or expired token' });
}
};
// Role-based middleware
const requireAdmin = (req, res, next) => {
if ([Link] !== 'admin') {
return [Link](403).json({ error: 'Admin access required' });
}
next();
};
[Link] = { auth, requireAdmin };
Using auth middleware on routes
const { auth, requireAdmin } = require('../middleware/auth');
// Protected route: any logged-in user
[Link]('/profile', auth, async (req, res) => {
const user = await [Link]([Link]).select('-password');
[Link](user);
});
// Admin-only route
[Link]('/users/:id', auth, requireAdmin, async (req, res) => {
await [Link]([Link]);
[Link]({ message: 'User deleted' });
});
6.4 Session-based vs JWT auth comparison
Feature Session-based JWT
Storage Server (memory/DB) Client (cookie/localStorage)
Stateful? Yes No (stateless)
Scalability Harder (shared sessions) Easier (no server state)
Revocation Easy (delete session) Hard (wait for expiry)
Best for Traditional web apps APIs, SPAs, mobile
7. Architectural Styles and Patterns
7.1 MVC (Model-View-Controller)
Separates application into three concerns:
• Model: Data and business logic (Mongoose schemas, database operations)
• View: Presentation layer (React components, templates)
• Controller: Handles requests, calls models, returns responses (Express route handlers)
// Model: models/[Link]
const productSchema = new [Link]({ name: String, price: Number });
// Controller: controllers/[Link]
[Link] = async (req, res) => {
const products = await [Link]();
[Link](products);
};
// Route (thin): routes/[Link]
const ctrl = require('../controllers/productController');
[Link]('/', [Link]);
7.2 Layered architecture
Separates code into horizontal layers. Each layer only communicates with the layer directly below it.
• Presentation layer: React frontend, handles UI
• API/Controller layer: Express routes, validates input, returns responses
• Service/Business logic layer: Contains rules, calculations, orchestration
• Data access layer: Mongoose models, database queries
• Database layer: MongoDB
EXAM TIP: The key rule of layered architecture: no layer should skip a layer. The controller
should never call the database directly; it should go through the service layer.
7.3 Client-Server architecture
The most fundamental web architecture pattern. The client (browser/React app) sends HTTP
requests to the server (Express API). The server processes requests, accesses the database, and
returns responses. They are decoupled: the client does not know how the server stores data, and
the server does not know how the client renders it.
7.4 Repository pattern
Abstracts the data layer behind an interface. The service layer calls repository methods instead of
directly using Mongoose. This makes it easier to swap databases or mock in tests.
// repositories/[Link]
const User = require('../models/User');
[Link] = {
findAll: () => [Link](),
findById: (id) => [Link](id),
create: (data) => [Link](data),
update: (id, data) => [Link](id, data, { new: true }),
delete: (id) => [Link](id),
};
// services/[Link]
const userRepo = require('../repositories/userRepository');
[Link] = () => [Link]();
[Link] = async (data) => {
[Link] = await [Link]([Link], 10);
return [Link](data);
};
7.5 Hexagonal architecture (Ports and Adapters)
The core business logic sits in the center, completely independent of frameworks, databases, or UIs.
It communicates with the outside world through ports (interfaces) and adapters (implementations).
• Core/Domain: Pure business logic with no external dependencies
• Ports: Interfaces that define how the core talks to the outside (e.g., UserRepository
interface)
• Adapters: Implementations that connect ports to real infrastructure (MongoDB adapter,
Express adapter, React adapter)
The key benefit: you can swap MongoDB for PostgreSQL by writing a new adapter without touching
business logic.
7.6 Full-stack flow diagram
For exam diagrams, draw this flow:
Browser (React)
|
| HTTP Request (fetch/axios)
v
Express Server
|
| Route -> Controller -> Service
v
Mongoose ODM
|
| Query
v
MongoDB Database
|
| Document(s)
v
Response flows back up: MongoDB -> Mongoose -> Service -> Controller ->
JSON Response -> React State Update -> UI Re-render
8. Miscellaneous Best Practices
8.1 Immutability patterns
Never mutate arrays or objects directly in React or Redux. Always create copies.
// Arrays
const added = [...arr, newItem];
const removed = [Link](x => [Link] !== id);
const updated = [Link](x => [Link] === id ? { ...x, done: true } : x);
// Objects
const updated = { ...obj, key: newValue };
const withoutKey = (({ keyToRemove, ...rest }) => rest)(obj);
8.2 Async/await error handling
// Pattern 1: try-catch in every route (verbose but explicit)
[Link]('/', async (req, res) => {
try {
const data = await [Link]();
[Link](data);
} catch (err) {
[Link](500).json({ error: [Link] });
}
});
// Pattern 2: Wrapper function (DRY)
const asyncHandler = (fn) => (req, res, next) =>
[Link](fn(req, res, next)).catch(next);
[Link]('/', asyncHandler(async (req, res) => {
const data = await [Link]();
[Link](data);
}));
8.3 Search and filter patterns
// Backend: dynamic query building
[Link]('/', async (req, res) => {
const { search, category, minPrice, maxPrice, sort, page = 1 } =
[Link];
const query = {};
if (search) [Link] = { $regex: search, $options: 'i' };
if (category) [Link] = category;
if (minPrice || maxPrice) {
[Link] = {};
if (minPrice) [Link].$gte = Number(minPrice);
if (maxPrice) [Link].$lte = Number(maxPrice);
}
const limit = 10;
const skip = (page - 1) * limit;
const sortObj = sort === 'price_asc' ? { price: 1 } : { price: -1 };
const products = await
[Link](query).sort(sortObj).skip(skip).limit(limit);
const total = await [Link](query);
[Link]({ products, total, pages: [Link](total / limit) });
});
// Frontend: filter in React (client-side)
const filtered = [Link](p =>
[Link]().includes([Link]()) &&
(category === 'all' || [Link] === category)
);
8.4 HTTP status codes to remember
Code Meaning When to use
200 OK Successful GET, PUT, PATCH
201 Created Successful POST that creates a resource
204 No Content Successful DELETE (no body returned)
400 Bad Request Validation error, malformed input
401 Unauthorized Missing or invalid authentication
403 Forbidden Authenticated but lacks permission
404 Not Found Resource does not exist
500 Internal Server Error Unhandled server error
8.5 Common exam shortcuts
• For CORS errors: add cors() middleware with correct origin and credentials
• For 'cannot read property of undefined': check if the data has loaded before accessing
nested fields
• For empty [Link]: make sure [Link]() middleware is used
• For mongoose 'cast to ObjectId failed': validate the ID format before querying
• For stale state in useEffect: add the variable to the dependency array
• For 'each child should have unique key': add key={[Link]} (never use index as key)
• For cookie not being sent: check withCredentials on frontend and credentials: true in CORS
Exam Quick-Reference Sheet
Memorize this page. It covers the patterns you will write most often.
React one-liners
const [val, setVal] = useState(initialValue);
useEffect(() => { fetchData(); }, []);
{condition && <Component />}
{[Link](item => <Item key={[Link]} {...item} />)}
onChange={e => setVal([Link])}
onSubmit={e => { [Link](); handleSubmit(); }}
Redux Toolkit pattern
// Slice: createSlice({ name, initialState, reducers, extraReducers })
// Thunk: createAsyncThunk('name', async (arg) => { return data; })
// Component: useSelector(s => [Link]), useDispatch()
// extraReducers: [Link]([Link], (state, action) => {})
Express route template
[Link]('/', async (req, res) => {
try {
const data = await [Link]();
[Link](data);
} catch (err) {
[Link](500).json({ error: [Link] });
}
});
MongoDB query essentials
[Link]({ field: value })
[Link](id)
[Link](data)
[Link](id, data, { new: true, runValidators: true })
[Link](id)
[Link]({ name: { $regex: term, $options: 'i' } })
.sort({ field: 1 }).skip(offset).limit(count).populate('ref')
Auth in 4 lines
const hash = await [Link](password, 10); // Register
const match = await [Link](password, hash); // Login
const token = [Link]({ userId }, SECRET, { expiresIn: '1h' }); // Create
const decoded = [Link](token, SECRET); // Verify
Architecture keywords
• MVC: Model (data), View (UI), Controller (logic)
• Layered: Presentation, API, Service, Data Access, DB
• Client-Server: decoupled, HTTP-based communication
• Repository: abstraction over data layer, testability
• Hexagonal: core logic in center, ports and adapters around it
Top 10 exam mistakes to avoid
• 1. Forgetting [Link]() on form submit
• 2. Using array index as key in .map()
• 3. Missing [Link]() middleware (empty [Link])
• 4. Not passing { new: true } to findByIdAndUpdate
• 5. Mutating state directly instead of using setter
• 6. Async useEffect without inner function wrapper
• 7. Missing dependency array in useEffect (infinite loop)
• 8. Returning 200 for everything (use proper status codes)
• 9. Storing plain-text passwords (always bcrypt)
• 10. CORS credentials mismatch between frontend and backend