0% found this document useful (0 votes)
8 views31 pages

Backend SQL

This document outlines a comprehensive curriculum for backend development using Node.js, Express, TypeScript, and MongoDB, including practical projects and key concepts. It covers various stages of development, including server setup, routing, MongoDB integration, middleware, validation, authentication, error handling, project structure, and testing. Each section provides detailed explanations and code examples to facilitate learning and implementation.

Uploaded by

ipsita lahiri
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
8 views31 pages

Backend SQL

This document outlines a comprehensive curriculum for backend development using Node.js, Express, TypeScript, and MongoDB, including practical projects and key concepts. It covers various stages of development, including server setup, routing, MongoDB integration, middleware, validation, authentication, error handling, project structure, and testing. Each section provides detailed explanations and code examples to facilitate learning and implementation.

Uploaded by

ipsita lahiri
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

Backend Development

Complete Reference — Curriculum + Extended Syllabus

[Link] + Express + TypeScript + MongoDB


Expense Tracker Project + SQL + Auth + API Design

Section Topics Status

Stages 1–10 Express, MongoDB, Auth, Testing, Deployment Built & Deployed

SQL DDL, DML, Joins, Aggregation, Window Functions, ACID Syllabus

Auth Deep Dive OAuth2, Refresh Tokens, RBAC, Vulnerabilities Syllabus

API Design REST, Pagination, Rate Limiting, CORS, Versioning Syllabus


Stage 1 — Setup & Server Basics

What We Built
A running Express server with TypeScript, configured from scratch.

Key Concepts
[Link] — Runtime that lets JavaScript run outside the browser. Everything else sits on top.

npm — Installs libraries. [Link] tracks everything about your project.

TypeScript — JavaScript with types. Compiles to JS before running. Catches mistakes early.

ts-node — Run .ts files directly during development without compiling first.

nodemon — Watches files, auto-restarts server on save.

Ports — Port 3000 is a door number. localhost:3000 = 'this computer, door 3000.'

Request/Response Cycle — Browser sends request → server processes → sends response.

Commands
npm init -y # Creates [Link]
npm install typescript ts-node @types/node --save-dev # TypeScript tools
npx tsc --init # Creates [Link]
npm install express # Install Express
npm install @types/express --save-dev # TS types for Express
npm install nodemon --save-dev # Auto-restart

[Link]
{
"compilerOptions": {
"target": "ES2020", // Compile to ES2020 JavaScript
"module": "commonjs", // Use require() style — what Node expects
"rootDir": "./src", // TypeScript source files live here
"outDir": "./dist", // Compiled JavaScript output goes here
"strict": true, // Strict type checking
"esModuleInterop": true, // Lets you write: import express from 'express'
"skipLibCheck": true, // Don't type-check node_modules
"types": ["node"] // Tell TypeScript about Node's built-ins
},
"include": ["src/**/*"],
"exclude": ["node_modules", "src/__tests__"]
}

Why CommonJS not ES Modules?


Node invented CommonJS (require/[Link]) before JS had a standard import system. You write
import/export in TypeScript and it compiles down to require() — clean syntax, Node-compatible output.

[Link] scripts
"scripts": {
"dev": "nodemon --exec ts-node src/[Link]", // Development
"build": "npx tsc", // Compile TS -> JS
"start": "node dist/[Link]", // Production
"test": "jest --forceExit" // Run tests
}

First Express Server


import express from 'express'

const app = express()


const PORT = [Link] || 3000

[Link]([Link]()) // Parse incoming JSON bodies

[Link]('/', (req, res) => { // Route: GET / returns JSON


[Link]({ message: 'expense tracker api is running' })
})

[Link](PORT, () => {
[Link](`Server running on port ${PORT}`)
})

export default app


Stage 2 — Routing & Controllers

Key Concepts
Route — HTTP method + URL path. GET /expenses and POST /expenses are different routes.

Controller — Function that runs when route matches. Contains logic, kept separate from routes.

Router() — Mini Express app for grouping routes. Attached with [Link]('/prefix', router).

REST Conventions
GET /expenses → getAllExpenses (fetch all)
POST /expenses → createExpense (create new)
PUT /expenses/:id → updateExpense (update specific)
DELETE /expenses/:id → deleteExpense (delete specific)

Three Ways Data Enters Your Server


[Link] — URL path: /expenses/123 → [Link] = '123'

[Link] — Query string: /expenses?month=jan → [Link] = 'jan'

[Link] — Request body: POST/PUT send JSON data here

src/routes/[Link]
import { Router } from 'express'
import { getAllExpenses, createExpense, updateExpense, deleteExpense }
from '../controllers/[Link]'
import validate from '../middleware/validate'
import { createExpenseSchema, updateExpenseSchema } from '../validators/[Link]'
import protect from '../middleware/auth'

const router = Router()

[Link](protect) // All routes require authentication

[Link]('/', getAllExpenses)
[Link]('/', validate(createExpenseSchema), createExpense)
[Link]('/:id', validate(updateExpenseSchema), updateExpense)
[Link]('/:id', deleteExpense)

export default router

HTTP Status Codes


200 OK // Request succeeded
201 Created // New resource created (use for POST)
400 Bad Request // Client sent invalid data
401 Unauthorized // Not authenticated
404 Not Found // Resource doesn't exist
500 Server Error // Something broke on the server
Stage 3 — MongoDB + Mongoose

Key Concepts
MongoDB — Stores data as documents (JSON-like). Collections = tables, Documents = rows.

MongoDB Atlas — Cloud-hosted MongoDB. Your DB lives on their servers 24/7.

Mongoose — Sits between your app and MongoDB. Schemas, models, clean query functions.

Schema — Defines data shape and rules. Types, required fields, defaults.

Model — Object you use to interact with collection. [Link](), [Link]() etc.

.env and dotenv


# .env — NEVER commit this
MONGO_URI=mongodb://user:pass@host/db
JWT_SECRET=yoursecretkey
PORT=3000

# src/[Link] — must be first


import dotenv from 'dotenv'
[Link]() // Loads .env into [Link]

src/config/[Link]
import mongoose from 'mongoose'

const connectDB = async () => {


try {
const conn = await [Link]([Link].MONGO_URI as string)
[Link](`MongoDB connected: ${[Link]}`)
} catch (error) {
[Link]('MongoDB connection error:', error)
[Link](1) // Kill server — no point running without DB
}
}

export default connectDB

src/models/[Link]
import mongoose from 'mongoose'

const expenseSchema = new [Link]({


title: { type: String, required: true },
amount: { type: Number, required: true },
category: { type: String, required: true },
date: { type: Date, default: [Link] } // Auto-fills if not provided
})

// 'Expense' → collection becomes 'expenses' (lowercase + plural)


const Expense = [Link]('Expense', expenseSchema)
export default Expense
Mongoose CRUD
[Link]() // Get all
[Link](id) // Get one
[Link]({ title, amount, category }) // Create and save
[Link](id, data, {new: true}) // Update, return updated
[Link](id) // Delete
Stage 4 — Middleware

Key Concepts
Middleware — Functions running between request and response. Think checkpoints.

next() — Pass request to next thing in chain. Forgetting it causes requests to hang forever.

Order matters — Express reads top to bottom. json → logger → routes → 404 → error handler.

Middleware Signatures
// Regular middleware: 3 parameters
(req: Request, res: Response, next: NextFunction) => { next() }

// Error middleware: 4 parameters — Express detects this automatically


(err: Error, req: Request, res: Response, next: NextFunction) => { }

src/middleware/[Link]
import { Request, Response, NextFunction } from 'express'

const logger = (req: Request, res: Response, next: NextFunction) => {


[Link](`${[Link]} ${[Link]} - ${new Date().toISOString()}`)
next() // Critical — without this, request hangs
}

export default logger

Middleware Order in [Link]


[Link]([Link]()) // 1. Parse JSON bodies
[Link](logger) // 2. Log every request
[Link]('/auth', authRouter) // 3. Routes
[Link]('/expenses', expenseRouter)
[Link]((req, res) => { // 4. 404 — after all routes
[Link](404).json({ message: `Route ${[Link]} not found` })
})
[Link]((err, req, res, next) => { // 5. Error handler — last
[Link]([Link] || 500).json({ message: [Link] })
})
Stage 5 — Validation (Zod)
Rule: Never trust user input. Validate everything before it touches your database.

Zod — TypeScript-first validation. Parse data against a schema. Failure = clean error. Success = typed object.

safeParse — Never throws. Returns { success: true, data } or { success: false, error }.

src/validators/[Link]
import { z } from 'zod'

export const createExpenseSchema = [Link]({


title: [Link]().min(1, 'Title is required'),
amount: [Link]().positive('Amount must be positive'),
category: [Link]().min(1, 'Category is required'),
date: [Link]().optional()
})

export const updateExpenseSchema = [Link]({


title: [Link]().min(1).optional(), // Everything optional for updates
amount: [Link]().positive().optional(),
category: [Link]().min(1).optional(),
date: [Link]().optional()
})

src/middleware/[Link]
const validate = (schema: ZodSchema) => {
return (req: Request, res: Response, next: NextFunction) => {
const result = [Link]([Link])

if (![Link]) {
[Link](400).json({
message: 'Validation failed',
errors: [Link]().fieldErrors
})
return
}

[Link] = [Link] // Clean, typed, validated data


next()
}
}
Stage 6 — Authentication
bcrypt — Password hashing. Never store plaintext. Scrambles password into hash. Compares on login.

JWT — Token issued after login. Contains encoded userId + signature. Only your server can verify.

JWT Structure
[Link]
- header: algorithm used
- payload: { userId: '123' } — anyone can decode
- signature: proves not tampered. Only verifiable with JWT_SECRET

src/controllers/[Link]
export const register = async (req: Request, res: Response) => {
const { email, password } = [Link]
const existingUser = await [Link]({ email })
if (existingUser) { [Link](400).json({ message: 'User already exists' }); return }

const hashedPassword = await [Link](password, 10) // 10 = salt rounds


await [Link]({ email, password: hashedPassword })
[Link](201).json({ message: 'User created successfully' })
}

export const login = async (req: Request, res: Response) => {


const { email, password } = [Link]
const user = await [Link]({ email })
if (!user) { [Link](401).json({ message: 'Invalid credentials' }); return }
// Same error for wrong email AND wrong password — don't help attackers

const isMatch = await [Link](password, [Link] as string)


if (!isMatch) { [Link](401).json({ message: 'Invalid credentials' }); return }

const token = [Link]({ userId: user._id }, [Link].JWT_SECRET as string, { expiresIn: '7d' })


[Link]({ token })
}

src/middleware/[Link]
export interface AuthRequest extends Request {
userId?: string
}

const protect = (req: AuthRequest, res: Response, next: NextFunction) => {


const authHeader = [Link]
if (!authHeader || ![Link]('Bearer ')) {
[Link](401).json({ message: 'No token provided' }); return
}
const token = [Link](' ')[1]
try {
const decoded = [Link](token, [Link].JWT_SECRET as string) as { userId: string }
[Link] = [Link]
next()
} catch (error) {
[Link](401).json({ message: 'Invalid token' })
}
}
Stage 7 — Error Handling Properly

src/utils/[Link]
class AppError extends Error {
statusCode: number
constructor(message: string, statusCode: number) {
super(message)
[Link] = statusCode
}
}
// Usage: throw new AppError('Expense not found', 404)

src/utils/[Link]
const asyncHandler = (fn: AsyncController) => {
return (req: Request, res: Response, next: NextFunction) => {
fn(req, res, next).catch(next)
// .catch(next) = if anything throws, pass to global error handler
}
}

Clean Controllers — No try/catch


export const updateExpense = asyncHandler(async (req: AuthRequest, res: Response) => {
const expense = await [Link]([Link], [Link], { new: true })
if (!expense) throw new AppError('Expense not found', 404)
[Link](expense)
})

Error Flow
throw new AppError('Not found', 404)
→ [Link](next)
→ next(error)
→ Global error handler (4 params)
→ [Link](404).json({ message: 'Not found' })
Stage 8 — Project Structure
expensetracker/
■■■ src/
■ ■■■ __tests__/[Link]
■ ■■■ config/[Link]
■ ■■■ controllers/
■ ■ ■■■ [Link]
■ ■ ■■■ [Link]
■ ■■■ middleware/
■ ■ ■■■ [Link]
■ ■ ■■■ [Link]
■ ■ ■■■ [Link]
■ ■■■ models/
■ ■ ■■■ [Link]
■ ■ ■■■ [Link]
■ ■■■ routes/
■ ■ ■■■ [Link]
■ ■ ■■■ [Link]
■ ■■■ types/[Link]
■ ■■■ utils/
■ ■ ■■■ [Link]
■ ■ ■■■ [Link]
■ ■■■ validators/
■ ■ ■■■ [Link]
■ ■ ■■■ [Link]
■ ■■■ [Link] ← app setup, exports app, NO listen()
■ ■■■ [Link] ← only: [Link]()
■■■ .env
■■■ .gitignore
■■■ [Link]
■■■ [Link]

[Link] and [Link] are separate so tests can import the app without starting a real server on a port.
const PORT = [Link] || 3000
// Railway sets PORT automatically. Never hardcode it.
Stage 9 — Testing
Jest — Test runner. Finds and runs test files, reports results.

Supertest — Makes HTTP requests to Express app without starting a real server.

ts-jest — Lets Jest understand TypeScript.

src/__tests__/[Link]
import request from 'supertest'
import app from '../index'

describe('Auth Routes', () => {

it('should register a new user', async () => {


const res = await request(app)
.post('/auth/register')
.send({ email: `test${[Link]()}@[Link]`, password: 'password123' })
expect([Link]).toBe(201)
expect([Link]).toBe('User created successfully')
})

it('should reject login with wrong password', async () => {


const res = await request(app)
.post('/auth/login')
.send({ email: 'test@[Link]', password: 'wrongpassword' })
expect([Link]).toBe(401)
})
})

PASS src/__tests__/[Link]
Auth Routes
v should register a new user (1104ms)
v should login with valid credentials (350ms)
v should reject login with wrong password (144ms)
Tests: 3 passed, 3 total
Stage 10 — Deployment
Stack: MongoDB Atlas (DB) + GitHub (code) + Railway (hosting)

Build vs Start
# Build (Railway runs once when deploying)
npm install --include=dev && npx tsc
# --include=dev: installs TypeScript, @types/* needed to compile

# Start (Railway runs to launch server)


node dist/[Link]

Railway Variables
MONGO_URI = mongodb://user:pass@host/db
JWT_SECRET = yoursecretkey
NODE_ENV = production
# Don't set PORT — Railway sets it automatically

Deployment Checklist
[ ] .gitignore: node_modules, .env, dist
[ ] [Link]: build and start scripts defined
[ ] PORT: uses [Link] || 3000
[ ] All secrets in Railway Variables tab
[ ] MongoDB Atlas: IP whitelist [Link]/0
[ ] Railway: Generate Domain matching correct port
[ ] Test: GET [Link]
SQL — Complete Syllabus
The biggest gap from the curriculum. Most backend tests use SQL, not MongoDB. ~40% of what online tests
check.

Foundations
What a relational database is vs document DB (MongoDB) — tables/rows vs collections/documents.

Data types: INT, VARCHAR, TEXT, BOOLEAN, DATE, TIMESTAMP

NULL — what it means, why it's dangerous, IS NULL vs = NULL (= NULL never works, always use IS NULL)

Primary key — uniquely identifies a row. Foreign key — references primary key of another table.

DDL — Data Definition Language


CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
email VARCHAR(255) NOT NULL UNIQUE,
name VARCHAR(100) NOT NULL,
role VARCHAR(50) DEFAULT 'user',
created TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

ALTER TABLE users ADD COLUMN age INT;


ALTER TABLE users DROP COLUMN age;
ALTER TABLE users MODIFY COLUMN name VARCHAR(200);

DROP TABLE users; -- Deletes table and all data


TRUNCATE TABLE users; -- Deletes all rows, keeps table structure
-- TRUNCATE vs DELETE: TRUNCATE is faster, can't use WHERE, resets auto-increment

DML — Data Manipulation Language


-- SELECT
SELECT * FROM users;
SELECT id, name, email FROM users;
SELECT name AS user_name FROM users; -- Alias

-- WHERE
SELECT * FROM users WHERE role = 'admin';
SELECT * FROM users WHERE age > 18 AND role = 'user';
SELECT * FROM users WHERE role IN ('admin', 'moderator');
SELECT * FROM users WHERE name LIKE 'S%'; -- Starts with S
SELECT * FROM users WHERE name LIKE '%ha'; -- Ends with ha
SELECT * FROM users WHERE name LIKE '%resh%'; -- Contains resh
SELECT * FROM users WHERE age BETWEEN 18 AND 25;
SELECT * FROM users WHERE email IS NULL;
SELECT * FROM users WHERE email IS NOT NULL;

-- ORDER BY, LIMIT, OFFSET


SELECT * FROM users ORDER BY name ASC;
SELECT * FROM users ORDER BY created DESC LIMIT 10;
SELECT * FROM users ORDER BY created DESC LIMIT 10 OFFSET 20; -- Page 3
-- INSERT
INSERT INTO users (email, name) VALUES ('s@[Link]', 'Sreshtha');
INSERT INTO users (email, name) VALUES ('a@[Link]','A'), ('b@[Link]','B');

-- UPDATE (ALWAYS use WHERE or you update every row)


UPDATE users SET role = 'admin' WHERE id = 1;
UPDATE users SET role = 'user', name = 'New' WHERE email = 'test@[Link]';

-- DELETE (ALWAYS use WHERE or you delete every row)


DELETE FROM users WHERE id = 5;
DELETE FROM users WHERE created < '2024-01-01';

Aggregations
SELECT COUNT(*) FROM users; -- Total rows
SELECT COUNT(email) FROM users; -- Non-NULL emails only
SELECT SUM(amount) FROM expenses;
SELECT AVG(amount) FROM expenses;
SELECT MIN(amount), MAX(amount) FROM expenses;

-- GROUP BY
SELECT category, COUNT(*) as total FROM expenses GROUP BY category;
SELECT category, SUM(amount) as total FROM expenses GROUP BY category;
SELECT category, AVG(amount) as avg_spend FROM expenses GROUP BY category ORDER BY avg_spend DESC;

-- HAVING (filter after grouping — WHERE filters before grouping)


SELECT category, SUM(amount) as total
FROM expenses
GROUP BY category
HAVING total > 1000; -- Only categories where total > 1000

-- DISTINCT
SELECT DISTINCT category FROM expenses;

Joins — Most Tested SQL Topic


-- Setup for examples:
-- users: id, name, email
-- expenses: id, user_id, title, amount, category

-- INNER JOIN: only rows that match in BOTH tables


SELECT [Link], [Link], [Link]
FROM users u
INNER JOIN expenses e ON [Link] = e.user_id;

-- LEFT JOIN: ALL rows from left (users), matching from right (expenses)
-- Users with no expenses get NULL for expense columns
SELECT [Link], [Link]
FROM users u
LEFT JOIN expenses e ON [Link] = e.user_id;

-- RIGHT JOIN: ALL rows from right, matching from left (rarely used)
SELECT [Link], [Link]
FROM users u
RIGHT JOIN expenses e ON [Link] = e.user_id;

-- FULL OUTER JOIN: everything from both tables


SELECT [Link], [Link]
FROM users u
FULL OUTER JOIN expenses e ON [Link] = e.user_id;

-- Multiple joins
SELECT [Link], [Link], [Link] as category_label
FROM users u
JOIN expenses e ON [Link] = e.user_id
JOIN categories c ON e.category_id = [Link];

-- Self JOIN: table joins itself (find manager of each employee)


SELECT [Link] as employee, [Link] as manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = [Link];

Subqueries
-- Subquery in WHERE
SELECT * FROM users WHERE id IN (SELECT user_id FROM expenses WHERE amount > 500);

-- Subquery with EXISTS


SELECT * FROM users u
WHERE EXISTS (SELECT 1 FROM expenses e WHERE e.user_id = [Link] AND [Link] > 1000);

-- Subquery in FROM (derived table)


SELECT category, avg_amount
FROM (
SELECT category, AVG(amount) as avg_amount FROM expenses GROUP BY category
) as category_avgs
WHERE avg_amount > 200;

-- Scalar subquery in SELECT


SELECT name, (SELECT COUNT(*) FROM expenses WHERE user_id = [Link]) as expense_count
FROM users u;

-- Correlated subquery (runs once per outer row — slow but powerful)
SELECT * FROM expenses e
WHERE amount > (SELECT AVG(amount) FROM expenses WHERE category = [Link]);

Window Functions
-- ROW_NUMBER: unique number per row within partition
SELECT name, amount, category,
ROW_NUMBER() OVER (PARTITION BY category ORDER BY amount DESC) as rank_in_category
FROM expenses;

-- RANK vs DENSE_RANK
-- RANK: 1,2,2,4 (skips 3 after tie)
-- DENSE_RANK: 1,2,2,3 (no skipping)
SELECT name, amount,
RANK() OVER (ORDER BY amount DESC) as rank,
DENSE_RANK() OVER (ORDER BY amount DESC) as dense_rank
FROM expenses;

-- Running total
SELECT title, amount, SUM(amount) OVER (ORDER BY date) as running_total FROM expenses;
-- LEAD and LAG (next/previous row value)
SELECT title, amount,
LAG(amount) OVER (ORDER BY date) as prev_amount,
LEAD(amount) OVER (ORDER BY date) as next_amount
FROM expenses;

-- Top N per group (classic interview question)


SELECT * FROM (
SELECT *, ROW_NUMBER() OVER (PARTITION BY category ORDER BY amount DESC) as rn
FROM expenses
) ranked
WHERE rn <= 3; -- Top 3 expenses per category

Indexes
What an index is — A sorted copy of a column stored separately for fast lookup. Like a book's index — you
find the page without reading the whole book.

B-tree index — Default. Good for equality and range queries. Keeps data sorted.

Reads vs Writes — Indexes speed up reads but slow writes (index must be updated on every
INSERT/UPDATE/DELETE).

When to index — Columns used in WHERE, JOIN conditions, ORDER BY. Foreign keys. High-cardinality
columns.

When NOT to index — Small tables, columns rarely queried, columns with very few distinct values (boolean),
tables with heavy write load.
CREATE INDEX idx_email ON users(email);
CREATE INDEX idx_category_amount ON expenses(category, amount); -- Composite
-- Composite index: useful when both columns are in WHERE together
-- Order matters: idx(category, amount) helps WHERE category=X AND amount>Y
-- But NOT WHERE amount>Y alone (leftmost prefix rule)

DROP INDEX idx_email ON users;

Transactions and ACID


Atomicity — All or nothing. If any part fails, the whole transaction rolls back.

Consistency — Transaction takes DB from one valid state to another.

Isolation — Concurrent transactions don't interfere with each other.

Durability — Committed transactions survive crashes.


BEGIN; -- Start transaction
UPDATE accounts SET balance = balance - 500 WHERE id = 1;
UPDATE accounts SET balance = balance + 500 WHERE id = 2;
COMMIT; -- Both succeed, save changes

-- If anything fails:
ROLLBACK; -- Undo everything

-- Savepoints
BEGIN;
UPDATE ...;
SAVEPOINT sp1;
UPDATE ...; -- If this fails:
ROLLBACK TO sp1; -- Roll back to savepoint, not beginning
COMMIT;

Dirty read — Reading uncommitted data from another transaction.

Non-repeatable read — Reading same row twice, getting different values.

Phantom read — Query returns different rows when run twice.

Isolation levels from weakest to strongest: READ UNCOMMITTED → READ COMMITTED → REPEATABLE
READ → SERIALIZABLE

Database Design
1NF — No repeating groups, atomic values (no arrays in columns).

2NF — 1NF + no partial dependency (non-key columns depend on entire primary key).

3NF — 2NF + no transitive dependency (non-key columns don't depend on other non-key columns).

Denormalization — Intentionally breaking normal forms for performance. Store redundant data to avoid
expensive joins.
-- One-to-many: one user has many expenses
users: id, name
expenses: id, user_id (FK → [Link]), amount

-- Many-to-many: users have many tags, tags have many users


users: id, name
tags: id, label
user_tags: user_id (FK), tag_id (FK) -- Junction table

-- One-to-one: user has one profile


users: id, name
profiles: id, user_id (FK UNIQUE), bio

Classic SQL Interview Problems


-- Second highest salary
SELECT MAX(salary) FROM employees WHERE salary < (SELECT MAX(salary) FROM employees);
-- Or:
SELECT salary FROM employees ORDER BY salary DESC LIMIT 1 OFFSET 1;

-- Employees who earn more than their manager


SELECT [Link] FROM employees e JOIN employees m ON e.manager_id = [Link]
WHERE [Link] > [Link];

-- Duplicate emails
SELECT email FROM users GROUP BY email HAVING COUNT(*) > 1;

-- Users who never made an expense (LEFT JOIN + IS NULL trick)


SELECT [Link] FROM users u
LEFT JOIN expenses e ON [Link] = e.user_id
WHERE [Link] IS NULL;
-- Nth highest value (using window function)
SELECT amount FROM (
SELECT amount, DENSE_RANK() OVER (ORDER BY amount DESC) as rnk FROM expenses
) ranked WHERE rnk = 3; -- 3rd highest

Practice: [Link], [Link], LeetCode SQL section (Easy → Medium)


Auth — Deep Dive

Password Security
Why not plaintext — DB leaks happen. Plaintext = everyone's password exposed instantly.

Hashing vs Encryption — Encryption is reversible (you can decrypt). Hashing is one-way (you can't unhash).
Passwords must be hashed, never encrypted.

Salt — Random string added to password before hashing. Means same password hashes differently each time.
Defeats rainbow table attacks (precomputed hash lookups).

bcrypt — Industry standard. Slow by design (prevents brute force). Salt is built in. Salt rounds = 10 means 2^10
= 1024 hashing iterations.

Never — log passwords, return them in API responses, store in localStorage with other sensitive data.
// Register
const hashedPassword = await [Link](password, 10)
// hash('password123', 10) → '$2b$10$xyz...' (different each time due to salt)

// Login
const isMatch = await [Link](plaintext, storedHash)
// compare('password123', '$2b$10$xyz...') → true/false

JWT Deep Dive


Three parts — [Link] (separated by dots)

Header — Algorithm: { alg: 'HS256', typ: 'JWT' } — base64 encoded

Payload — Your data: { userId, iat, exp } — base64 encoded. Anyone can decode this. Never put sensitive
data here.

Signature — HMAC(base64(header) + '.' + base64(payload), secret). Only your server can create or verify this.

HS256 — Symmetric. Same secret signs and verifies. For single-server apps.

RS256 — Asymmetric. Private key signs, public key verifies. For multi-service architectures.

Claims — iss (issuer), exp (expiry unix timestamp), iat (issued at), sub (subject/userId), jti (unique ID).
[Link]({ userId: user._id }, secret, { expiresIn: '7d' })
// exp is automatically set to now + 7 days

[Link](token, secret)
// Throws if: signature invalid, expired, malformed

// JWT vulnerabilities to know:


// 1. 'none' algorithm attack — attacker sets alg:none, no signature needed
// Fix: always specify algorithm in verify(): [Link](token, secret, { algorithms: ['HS256'] })
// 2. Weak secret — brute-forceable with tools like hashcat
// Fix: use long random secret (32+ chars)
// 3. No expiry — tokens valid forever
// Fix: always set expiresIn

Session vs JWT
Session-based Auth: JWT Auth:
■■■■■■■■■■■■■■■■■■■■■■■■■■■■ ■■■■■■■■■■■■■■■■■■■■■■■■■■■■
■ Server stores session in DB/Redis ■ ■ Server stores NOTHING ■
■ Client gets session ID cookie ■ ■ Client stores JWT token ■
■ Every request: lookup session ■ ■ Every request: verify sig ■
■ Easy to revoke (delete session) ■ ■ Hard to revoke (wait expiry)■
■ Stateful ■ ■ Stateless ■
■ Bad for horizontal scaling ■ ■ Good for horizontal scaling■
■■■■■■■■■■■■■■■■■■■■■■■■■■■■ ■■■■■■■■■■■■■■■■■■■■■■■■■■■■

Use sessions when: you need instant revocation, server-rendered apps


Use JWT when: stateless API, mobile clients, microservices

Access + Refresh Token Pattern


Access token — Short-lived (15 min). Used for API calls. If stolen, attacker has 15 min max.

Refresh token — Long-lived (30 days). Stored securely (httpOnly cookie). Used ONLY to get new access
token.

Flow — Login → get both tokens. Access token expires → send refresh token → get new access token.
Refresh token expires → login again.

Refresh token rotation — Each time refresh token is used, a new one is issued and old is invalidated. Limits
replay attacks.
// Login response
{ accessToken: 'eyJ...', refreshToken: 'eyJ...' }

// POST /auth/refresh
// Body: { refreshToken }
// Server: verify refresh token, issue new access token
// Response: { accessToken: 'eyJ...' }

OAuth2 Flow (Conceptual)


OAuth2 = authorization framework. Lets users grant your app access to their data on another service (Google,
GitHub) without sharing passwords.
Authorization Code Flow (most common):

1. User clicks 'Login with Google'


2. Your app redirects to Google's auth server:
[Link]
client_id=YOUR_ID&redirect_uri=YOUR_CALLBACK&scope=email profile

3. User logs in on Google, approves permissions


4. Google redirects back: [Link]

5. Your server exchanges code for tokens (server-to-server, no user involved):


POST [Link]
{ code, client_id, client_secret, redirect_uri }

6. Google returns: { access_token, id_token, refresh_token }

7. Your server decodes id_token to get user info (email, name)


8. Create/find user in your DB, issue YOUR JWT

OAuth — Authorization (what can this app do on your behalf).

OpenID Connect — Identity layer on top of OAuth2 (who you are). The id_token is OIDC.

Password Reset Flow


1. User POST /auth/forgot-password { email }
2. Generate cryptographically random token: [Link](32).toString('hex')
3. Hash it: const hashedToken = [Link]('sha256').update(token).digest('hex')
4. Store hashedToken + expiry in DB (not the raw token)
5. Send email with link: [Link]
6. User clicks link, sends: POST /auth/reset-password { token, newPassword }
7. Hash the received token, look up in DB
8. Check expiry
9. Update password, delete reset token from DB

Why store hash not raw token:


If DB is compromised, attacker can't use the stored hash to reset passwords.

RBAC — Role Based Access Control


// Store role in user model
const userSchema = new [Link]({
email: String,
password: String,
role: { type: String, enum: ['user', 'admin', 'moderator'], default: 'user' }
})

// Include role in JWT payload


[Link]({ userId: user._id, role: [Link] }, secret, { expiresIn: '7d' })

// Role middleware
const requireRole = (...roles: string[]) => {
return (req: AuthRequest, res: Response, next: NextFunction) => {
if (![Link]([Link]!)) {
[Link](403).json({ message: 'Forbidden' })
return
}
next()
}
}

// Usage
[Link]('/users/:id', protect, requireRole('admin'), deleteUser)

403 Forbidden = authenticated but not authorized. 401 Unauthorized = not authenticated.

Common Auth Vulnerabilities


CSRF — Cross-Site Request Forgery. Malicious site makes browser send requests to your API using stored
cookies. JWT in Authorization header is immune (browsers don't auto-send headers). Cookies are vulnerable.

XSS — Cross-Site Scripting. Injected scripts steal tokens from localStorage. httpOnly cookies can't be
accessed by JS.
Brute force — Rate limit login endpoint. Lock account after N failures. Add delay after failures.

JWT none algorithm — Always specify algorithm in verify(). Never trust the alg from the token header.

Secret compromise — All tokens invalid immediately. Must rotate secret and force re-login.
API Design — Complete Syllabus

REST Principles
Stateless — Every request contains everything the server needs. Server stores no client state between
requests. JWT is the mechanism for this.

Resource naming — Nouns, not verbs. /expenses not /getExpenses not /createExpense.

Plural nouns — /expenses not /expense. /users not /user.

Nested resources — /users/:id/expenses (expenses belonging to a user).


HTTP Methods — correct usage:
GET → Read. Safe (no side effects) + Idempotent
POST → Create. Neither safe nor idempotent
PUT → Replace entire resource. Idempotent
PATCH → Partial update. Not necessarily idempotent
DELETE → Delete. Idempotent

Idempotent = calling N times = same result as calling once


GET /expenses/1 called 10 times → always returns same expense
DELETE /expenses/1 called 10 times → first deletes, rest return 404. Still idempotent.
POST /expenses called 10 times → creates 10 expenses. NOT idempotent.

Pagination
Offset/Limit — Simple. ?page=2&limit;=10. Problems: slow on large tables (DB scans all preceding rows),
inconsistent if rows inserted during pagination.

Cursor-based — ?cursor=lastId&limit;=10. Fast (uses indexed ID), consistent, but can't jump to arbitrary
pages.
// Offset pagination
GET /expenses?page=2&limit=10
// SELECT * FROM expenses LIMIT 10 OFFSET 10
// Problem: OFFSET 10000 makes DB scan 10000 rows

// Cursor pagination
GET /expenses?cursor=6a0d45f1&limit=10
// SELECT * FROM expenses WHERE _id > cursor ORDER BY _id LIMIT 10
// Fast — uses indexed _id directly

// Response format
{
"data": [...],
"pagination": {
"nextCursor": "6a0eaf39", // null if no more pages
"hasMore": true,
"total": 247 // optional, expensive to compute
}
}

Filtering, Sorting, Searching


GET /expenses?category=food // Filter
GET /expenses?sort=amount&order=desc // Sort
GET /expenses?q=coffee // Search
GET /expenses?category=food&sort=date&order=asc&page=1&limit=20 // Combined

// Date range
GET /expenses?from=2026-01-01&to=2026-05-31

API Versioning
URL versioning — /v1/expenses. Most common. Explicit, easy to route, easy to deprecate.

Header versioning — Accept: application/[Link]+json;version=1. Cleaner URLs but harder to test in


browser.

Query param — /expenses?version=1. Easy but pollutes params.

Why version: breaking changes (removing fields, changing data types) need a new version so old clients don't
break.

Rate Limiting
What it is — Limit how many requests a client can make in a time window. Prevents abuse, DoS attacks.

Token bucket — Bucket holds N tokens. Each request consumes one. Bucket refills at rate R/second. Allows
bursting.

Sliding window — Count requests in rolling time window. More accurate, more memory.

Fixed window — Count requests per fixed window (per minute). Simple but allows double the rate at window
boundary.
// Express rate limiting (express-rate-limit library)
import rateLimit from 'express-rate-limit'

const limiter = rateLimit({


windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requests per window per IP
message: { message: 'Too many requests' },
standardHeaders: true, // Return rate limit info in headers
legacyHeaders: false,
})

[Link]('/auth', rateLimit({ windowMs: 60000, max: 5 })) // Stricter for login


[Link](limiter) // General limit

// Response headers
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 87
X-RateLimit-Reset: 1716000000
// Status 429 Too Many Requests when exceeded

CORS
What it is — Cross-Origin Resource Sharing. Browser security feature. By default, JS on [Link] can't
make requests to [Link] (same-origin policy).

Why it exists — Prevents malicious sites from making API calls using your logged-in session.
Preflight — Before complex requests, browser sends OPTIONS request asking 'is this allowed?' Server must
respond with allowed origins/methods.

Note: CORS is enforced by browsers only. curl, Postman, Thunder Client are not affected.
import cors from 'cors'

// Allow all origins (development)


[Link](cors())

// Allow specific origins (production)


[Link](cors({
origin: ['[Link] '[Link]
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true // Allow cookies to be sent cross-origin
}))

// Response headers CORS adds:


// Access-Control-Allow-Origin: [Link]
// Access-Control-Allow-Methods: GET, POST, PUT, DELETE
// Access-Control-Allow-Headers: Content-Type, Authorization

Error Response Design


// Consistent error format across all endpoints
{
"message": "Validation failed",
"code": "VALIDATION_ERROR", // Machine-readable code
"errors": { // Field-specific errors
"email": ["Invalid email format"],
"amount": ["Must be positive"]
},
"requestId": "abc-123" // For debugging/support
}

// Status code mapping


400 → Bad Request (validation, malformed body)
401 → Unauthorized (no token, invalid token)
403 → Forbidden (authenticated but no permission)
404 → Not Found (resource doesn't exist)
409 → Conflict (duplicate email, already exists)
422 → Unprocessable Entity (semantically wrong)
429 → Too Many Requests
500 → Internal Server Error (never expose details)
503 → Service Unavailable (DB down, maintenance)

// Never send this to client:


{ "error": "MongoError: duplicate key error collection: [Link]" }
// Always sanitize:
{ "message": "User already exists" }

REST vs GraphQL vs gRPC


REST:
+ Universal (every client understands HTTP + JSON)
+ Simple, cacheable, stateless
+ Easy to test (browser, curl, Thunder Client)
- Over-fetching (get whole user when you need just name)
- Under-fetching (need multiple requests for related data)
- No type safety across client/server

GraphQL:
+ Client specifies exactly what fields it needs
+ Single endpoint (/graphql)
+ Strongly typed schema
+ Great for complex, nested data
- Complex to implement
- Caching harder
- Learning curve

gRPC:
+ Binary protocol (10x faster than JSON)
+ Strongly typed (Protocol Buffers)
+ Perfect for internal service-to-service
- Not browser-native
- Not human-readable
- Overkill for most APIs

Rule of thumb:
Public API → REST
Complex data, multiple clients → GraphQL
Internal microservices, performance critical → gRPC

Webhooks
What they are — Your server gets called by an external service when something happens. Stripe calls you
when payment succeeds. GitHub calls you when code is pushed.

vs Polling — Polling: you ask 'anything new?' every N seconds. Webhook: they tell you immediately.
Webhooks are more efficient.
// Receiving a webhook
[Link]('/webhooks/stripe', [Link]({ type: 'application/json' }), (req, res) => {
const sig = [Link]['stripe-signature']

// Verify the webhook is genuinely from Stripe


const event = [Link]([Link], sig, [Link].STRIPE_WEBHOOK_SECRET)

switch ([Link]) {
case 'payment_intent.succeeded':
// Handle successful payment
break
}

[Link]({ received: true }) // Acknowledge quickly (within 30s)


})

Always verify webhook signatures. Attacker could send fake webhook events to your endpoint.

API Security
HTTPS always — HTTP transmits plaintext. Man-in-the-middle can read tokens, passwords, data.
Railway/Render provide HTTPS automatically.

[Link] — Sets security headers automatically: X-Frame-Options, X-Content-Type-Options,


Strict-Transport-Security etc.

Input sanitization — Prevent NoSQL injection ($where, $gt etc in MongoDB). Prevent SQL injection (use
parameterized queries, not string concatenation).

Sensitive data in responses — Never return passwords. Consider which fields to expose. Use
.select('-password') in Mongoose.
import helmet from 'helmet'
[Link](helmet()) // Sets ~15 security headers automatically

// Mongoose: exclude password from all queries


[Link] = function() {
const obj = [Link]()
delete [Link]
return obj
}

// Or per-query:
const user = await [Link](id).select('-password')
Quick Reference

JWT Full Flow


1. POST /auth/login { email, password }
2. [Link](password, storedHash) → true/false
3. [Link]({ userId }, secret, { expiresIn: '7d' })
4. Response: { token: 'eyJhbGc...' }
5. Client sends: Authorization: Bearer eyJhbGc...
6. protect middleware: [Link](token, secret) → { userId }
7. [Link] attached → controller uses it

Error Flow
throw new AppError('Not found', 404)
→ [Link](next)
→ next(error)
→ Global error handler (4 params)
→ [Link](404).json({ message: 'Not found' })

Request Flow — Protected Route


Request → [Link]() → logger → protect → validate → controller → Response

SQL Cheat Sheet


-- Most common pattern: filter + join + aggregate
SELECT [Link], COUNT([Link]) as expense_count, SUM([Link]) as total_spent
FROM users u
LEFT JOIN expenses e ON [Link] = e.user_id
WHERE [Link] > '2026-01-01'
GROUP BY [Link], [Link]
HAVING total_spent > 500
ORDER BY total_spent DESC
LIMIT 10;

-- Window function pattern


SELECT *, RANK() OVER (PARTITION BY category ORDER BY amount DESC) as rnk
FROM expenses;

-- Find duplicates
SELECT email, COUNT(*) FROM users GROUP BY email HAVING COUNT(*) > 1;

-- Rows in A but not in B


SELECT * FROM users u LEFT JOIN expenses e ON [Link] = e.user_id WHERE [Link] IS NULL;

Status Code Quick Ref


200 OK | 201 Created | 204 No Content
400 Bad Request | 401 Unauthorized | 403 Forbidden | 404 Not Found
409 Conflict | 422 Unprocessable | 429 Too Many Requests
500 Internal Error | 503 Service Unavailable
Stages 1–10 built and deployed. SQL + Auth + API Design syllabus complete.

API live: [Link]

You might also like